/* This file is part of Telegram Desktop, the official desktop application for the Telegram messaging service. For license and copyright information please follow this link: https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "media/view/media_view_group_thumbs.h" #include "data/data_shared_media.h" #include "data/data_user_photos.h" #include "data/data_photo.h" #include "data/data_photo_media.h" #include "data/data_document.h" #include "data/data_document_media.h" #include "data/data_media_types.h" #include "data/data_session.h" #include "data/data_web_page.h" #include "data/data_file_origin.h" #include "history/history.h" #include "history/view/media/history_view_media.h" #include "ui/image/image.h" #include "ui/ui_utility.h" #include "main/main_session.h" #include "core/crash_reports.h" #include "styles/style_media_view.h" namespace Media { namespace View { namespace { constexpr auto kThumbDuration = crl::time(150); int Round(float64 value) { return int(base::SafeRound(value)); } using Context = GroupThumbs::Context; using Key = GroupThumbs::Key; #if 0 [[nodiscard]] QString DebugSerializeMsgId(FullMsgId itemId) { return QString("msg%1_%2").arg(itemId.channel.bare).arg(itemId.msg); } [[nodiscard]] QString DebugSerializePeer(PeerId peerId) { return peerIsUser(peerId) ? QString("user%1").arg(peerToUser(peerId).bare) : peerIsChat(peerId) ? QString("chat%1").arg(peerToChat(peerId).bare) : QString("channel%1").arg(peerToChannel(peerId).bare); } [[nodiscard]] QString DebugSerializeKey(const Key &key) { return v::match(key, [&](PhotoId photoId) { return QString("photo%1").arg(photoId); }, [](FullMsgId itemId) { return DebugSerializeMsgId(itemId); }, [&](GroupThumbs::CollageKey key) { return QString("collage%1").arg(key.index); }); } [[nodiscard]] QString DebugSerializeContext(const Context &context) { return v::match(context, [](PeerId peerId) { return DebugSerializePeer(peerId); }, [](MessageGroupId groupId) { return QString("group_%1_%2" ).arg(DebugSerializePeer(groupId.peer) ).arg(groupId.value); }, [](FullMsgId item) { return DebugSerializeMsgId(item); }, [](v::null_t) -> QString { return "null"; }); } #endif Data::FileOrigin ComputeFileOrigin(const Key &key, const Context &context) { return v::match(key, [&](PhotoId photoId) { return v::match(context, [&](PeerId peerId) { return peerIsUser(peerId) ? Data::FileOriginUserPhoto(peerToUser(peerId), photoId) : Data::FileOrigin(Data::FileOriginPeerPhoto(peerId)); }, [](auto&&) { return Data::FileOrigin(); }); }, [](FullMsgId itemId) { return Data::FileOrigin(itemId); }, [&](GroupThumbs::CollageKey) { return v::match(context, [](const GroupThumbs::CollageSlice &slice) { return Data::FileOrigin(slice.context); }, [](auto&&) { return Data::FileOrigin(); }); }); } Context ComputeContext( not_null session, const SharedMediaWithLastSlice &slice, int index) { Expects(index >= 0 && index < slice.size()); const auto value = slice[index]; if (const auto photo = std::get_if>(&value)) { if (const auto peer = (*photo)->peer) { return peer->id; } return v::null; } else if (const auto msgId = std::get_if(&value)) { if (const auto item = session->data().message(*msgId)) { if (item->isService()) { return item->history()->peer->id; } else if (const auto groupId = item->groupId()) { return groupId; } } return v::null; } Unexpected("Variant in ComputeContext(SharedMediaWithLastSlice::Value)"); } Context ComputeContext( not_null session, const UserPhotosSlice &slice, int index) { return peerFromUser(slice.key().userId); } Context ComputeContext( not_null session, const GroupThumbs::CollageSlice &slice, int index) { return slice.context; } Key ComputeKey(const SharedMediaWithLastSlice &slice, int index) { Expects(index >= 0 && index < slice.size()); const auto value = slice[index]; if (const auto photo = std::get_if>(&value)) { return (*photo)->id; } else if (const auto msgId = std::get_if(&value)) { return *msgId; } Unexpected("Variant in ComputeKey(SharedMediaWithLastSlice::Value)"); } Key ComputeKey(const UserPhotosSlice &slice, int index) { return slice[index]; } Key ComputeKey(const GroupThumbs::CollageSlice &slice, int index) { return GroupThumbs::CollageKey{ index }; } int ComputeThumbsLimit(int availableWidth) { const auto singleWidth = st::mediaviewGroupWidth + 2 * st::mediaviewGroupSkip; const auto currentWidth = st::mediaviewGroupWidthMax + 2 * st::mediaviewGroupSkipCurrent; const auto skipForAnimation = 2 * singleWidth; const auto leftWidth = availableWidth - currentWidth - skipForAnimation; return std::max(leftWidth / (2 * singleWidth), 1); } } // namespace class GroupThumbs::Thumb { public: enum class State { Unknown, Current, Alive, Dying, }; Thumb(Key key, Fn handler); Thumb( Key key, not_null photo, Data::FileOrigin origin, Fn handler); Thumb( Key key, not_null document, Data::FileOrigin origin, Fn handler); [[nodiscard]] int leftToUpdate() const; [[nodiscard]] int rightToUpdate() const; void animateToLeft(not_null next); void animateToRight(not_null prev); void setState(State state); [[nodiscard]] State state() const; [[nodiscard]] bool inited() const; [[nodiscard]] bool removed() const; void paint(QPainter &p, int x, int y, int outerWidth, float64 progress); [[nodiscard]] ClickHandlerPtr getState(QPoint point) const; private: QSize wantedPixSize() const; void validateImage(); int currentLeft() const; int currentWidth() const; int finalLeft() const; int finalWidth() const; void animateTo(int left, int width); ClickHandlerPtr _link; const Key _key; std::shared_ptr _documentMedia; std::shared_ptr _photoMedia; Image *_image = nullptr; Data::FileOrigin _origin; State _state = State::Alive; QPixmap _full; int _fullWidth = 0; bool _hiding = false; anim::value _left = { 0. }; anim::value _width = { 0. }; anim::value _opacity = { 0., 1. }; }; GroupThumbs::Thumb::Thumb(Key key, Fn handler) : _key(key) { _link = std::make_shared(std::move(handler)); validateImage(); } GroupThumbs::Thumb::Thumb( Key key, not_null photo, Data::FileOrigin origin, Fn handler) : _key(key) , _photoMedia(photo->createMediaView()) , _origin(origin) { _link = std::make_shared(std::move(handler)); _photoMedia->wanted(Data::PhotoSize::Thumbnail, origin); validateImage(); } GroupThumbs::Thumb::Thumb( Key key, not_null document, Data::FileOrigin origin, Fn handler) : _key(key) , _documentMedia(document->createMediaView()) , _origin(origin) { _link = std::make_shared(std::move(handler)); _documentMedia->thumbnailWanted(origin); validateImage(); } QSize GroupThumbs::Thumb::wantedPixSize() const { const auto originalWidth = _image ? std::max(_image->width(), 1) : 1; const auto originalHeight = _image ? std::max(_image->height(), 1) : 1; const auto pixHeight = st::mediaviewGroupHeight; const auto pixWidth = originalWidth * pixHeight / originalHeight; return { pixWidth, pixHeight }; } void GroupThumbs::Thumb::validateImage() { if (!_image) { if (_photoMedia) { _image = _photoMedia->image(Data::PhotoSize::Thumbnail); } else if (_documentMedia) { _image = _documentMedia->thumbnail(); } } if (!_full.isNull() || !_image) { return; } const auto pixSize = wantedPixSize(); if (pixSize.width() > st::mediaviewGroupWidthMax) { const auto originalWidth = _image->width(); const auto originalHeight = _image->height(); const auto takeWidth = originalWidth * st::mediaviewGroupWidthMax / pixSize.width(); auto original = _image->original(); original.setDevicePixelRatio(cRetinaFactor()); _full = Ui::PixmapFromImage(original.copy( (originalWidth - takeWidth) / 2, 0, takeWidth, originalHeight ).scaled( st::mediaviewGroupWidthMax * cIntRetinaFactor(), pixSize.height() * cIntRetinaFactor(), Qt::IgnoreAspectRatio, Qt::SmoothTransformation)); } else { _full = _image->pixNoCache(pixSize * style::DevicePixelRatio()); } _fullWidth = std::min( wantedPixSize().width(), st::mediaviewGroupWidthMax); } int GroupThumbs::Thumb::leftToUpdate() const { return Round(std::min(_left.from(), _left.to())); } int GroupThumbs::Thumb::rightToUpdate() const { return Round(std::max( _left.from() + _width.from(), _left.to() + _width.to())); } int GroupThumbs::Thumb::currentLeft() const { return Round(_left.current()); } int GroupThumbs::Thumb::currentWidth() const { return Round(_width.current()); } int GroupThumbs::Thumb::finalLeft() const { return Round(_left.to()); } int GroupThumbs::Thumb::finalWidth() const { return Round(_width.to()); } void GroupThumbs::Thumb::setState(State state) { const auto isNewThumb = (_state == State::Alive); _state = state; if (_state == State::Current) { if (isNewThumb) { _opacity = anim::value(1.); _left = anim::value(-_fullWidth / 2); _width = anim::value(_fullWidth); } else { _opacity.start(1.); } _hiding = false; animateTo(-_fullWidth / 2, _fullWidth); } else if (_state == State::Alive) { _opacity.start(0.7); _hiding = false; } else if (_state == State::Dying) { _opacity.start(0.); _hiding = true; _left.restart(); _width.restart(); } } void GroupThumbs::Thumb::animateTo(int left, int width) { _left.start(left); _width.start(width); } void GroupThumbs::Thumb::animateToLeft(not_null next) { const auto width = st::mediaviewGroupWidth; if (_state == State::Alive) { // New item animation, start exactly from the next, move only. _left = anim::value(next->currentLeft() - width); _width = anim::value(width); } else if (_state == State::Unknown) { // Existing item animation. setState(State::Alive); } const auto skip1 = st::mediaviewGroupSkip; const auto skip2 = (next->state() == State::Current) ? st::mediaviewGroupSkipCurrent : st::mediaviewGroupSkip; animateTo(next->finalLeft() - width - skip1 - skip2, width); } void GroupThumbs::Thumb::animateToRight(not_null prev) { const auto width = st::mediaviewGroupWidth; if (_state == State::Alive) { // New item animation, start exactly from the next, move only. _left = anim::value(prev->currentLeft() + prev->currentWidth()); _width = anim::value(width); } else if (_state == State::Unknown) { // Existing item animation. setState(State::Alive); } const auto skip1 = st::mediaviewGroupSkip; const auto skip2 = (prev->state() == State::Current) ? st::mediaviewGroupSkipCurrent : st::mediaviewGroupSkip; animateTo(prev->finalLeft() + prev->finalWidth() + skip1 + skip2, width); } auto GroupThumbs::Thumb::state() const -> State { return _state; } bool GroupThumbs::Thumb::inited() const { return _fullWidth != 0; } bool GroupThumbs::Thumb::removed() const { return (_state == State::Dying) && _hiding && !_opacity.current(); } void GroupThumbs::Thumb::paint( QPainter &p, int x, int y, int outerWidth, float64 progress) { validateImage(); _opacity.update(progress, anim::linear); _left.update(progress, anim::linear); _width.update(progress, anim::linear); const auto left = x + currentLeft(); const auto width = currentWidth(); const auto opacity = p.opacity(); p.setOpacity(_opacity.current() * opacity); if (width == _fullWidth) { p.drawPixmap(left, y, _full); } else { const auto takeWidth = width * cIntRetinaFactor(); const auto from = QRect( (_full.width() - takeWidth) / 2, 0, takeWidth, _full.height()); const auto to = QRect(left, y, width, st::mediaviewGroupHeight); p.drawPixmap(to, _full, from); } p.setOpacity(opacity); } ClickHandlerPtr GroupThumbs::Thumb::getState(QPoint point) const { if (_state != State::Alive) { return nullptr; } const auto left = finalLeft(); const auto width = finalWidth(); return QRect(left, 0, width, st::mediaviewGroupHeight).contains(point) ? _link : nullptr; } int GroupThumbs::CollageSlice::size() const { return data->items.size(); } GroupThumbs::GroupThumbs(not_null session, Context context) : _session(session) , _context(context) { } void GroupThumbs::updateContext(Context context) { if (_context != context) { clear(); _context = context; } } template void GroupThumbs::RefreshFromSlice( not_null session, std::unique_ptr &instance, const Slice &slice, int index, int availableWidth) { const auto context = ComputeContext(session, slice, index); if (instance) { instance->updateContext(context); } if (v::is_null(context)) { if (instance) { instance->resizeToWidth(availableWidth); } return; } const auto limit = ComputeThumbsLimit(availableWidth); const auto from = [&] { const auto edge = std::max(index - limit, 0); for (auto result = index; result != edge; --result) { if (ComputeContext(session, slice, result - 1) != context) { return result; } } return edge; }(); const auto till = [&] { const auto edge = std::min(index + limit + 1, slice.size()); for (auto result = index + 1; result != edge; ++result) { if (ComputeContext(session, slice, result) != context) { return result; } } return edge; }(); if (from + 1 < till) { if (!instance) { instance = std::make_unique(session, context); } instance->fillItems(slice, from, index, till); instance->resizeToWidth(availableWidth); } else if (instance) { instance->clear(); instance->resizeToWidth(availableWidth); } } #if 0 template void ValidateSlice( const Slice &slice, const Context &context, int from, int index, int till) { auto keys = base::flat_set(); for (auto i = from; i != till; ++i) { const auto key = ComputeKey(slice, i); if (keys.contains(key)) { // All items should be unique! auto strings = QStringList(); strings.reserve(till - from); for (auto i = from; i != till; ++i) { strings.push_back(DebugSerializeKey(ComputeKey(slice, i))); } CrashReports::SetAnnotation( "keys", QString("%1:%2-(%3)-%4:" ).arg(DebugSerializeContext(context) ).arg(from ).arg(index ).arg(till) + strings.join(",")); if (Logs::DebugEnabled()) { Unexpected("Bad slice in GroupThumbs."); } break; } else { keys.emplace(key); } } } #endif template void GroupThumbs::fillItems( const Slice &slice, int from, int index, int till) { Expects(from <= index); Expects(index < till); Expects(from + 1 < till); const auto current = (index - from); const auto old = base::take(_items); //ValidateSlice(slice, _context, from, index, till); markCacheStale(); _items.reserve(till - from); for (auto i = from; i != till; ++i) { _items.push_back(validateCacheEntry(ComputeKey(slice, i))); } animateAliveItems(current); fillDyingItems(old); startDelayedAnimation(); } void GroupThumbs::animateAliveItems(int current) { Expects(current >= 0 && current < _items.size()); _items[current]->setState(Thumb::State::Current); for (auto i = current; i != 0;) { const auto prev = _items[i]; const auto item = _items[--i]; item->animateToLeft(prev); } for (auto i = current + 1; i != _items.size(); ++i) { const auto prev = _items[i - 1]; const auto item = _items[i]; item->animateToRight(prev); } } void GroupThumbs::fillDyingItems(const std::vector> &old) { //Expects(_cache.size() >= _items.size()); if (_cache.size() >= _items.size()) { _dying.reserve(_cache.size() - _items.size()); } animatePreviouslyAlive(old); markRestAsDying(); } void GroupThumbs::markRestAsDying() { //Expects(_cache.size() >= _items.size()); if (_cache.size() >= _items.size()) { _dying.reserve(_cache.size() - _items.size()); } for (const auto &cacheItem : _cache) { const auto &thumb = cacheItem.second; const auto state = thumb->state(); if (state == Thumb::State::Unknown) { markAsDying(thumb.get()); } } } void GroupThumbs::markAsDying(not_null thumb) { thumb->setState(Thumb::State::Dying); _dying.push_back(thumb.get()); } void GroupThumbs::animatePreviouslyAlive( const std::vector> &old) { auto toRight = false; for (auto i = 0; i != old.size(); ++i) { const auto item = old[i]; if (item->state() == Thumb::State::Unknown) { if (toRight) { markAsDying(item); item->animateToRight(old[i - 1]); } } else if (!toRight) { for (auto j = i; j != 0;) { const auto next = old[j]; const auto prev = old[--j]; markAsDying(prev); prev->animateToLeft(next); } toRight = true; } } } auto GroupThumbs::createThumb(Key key) -> std::unique_ptr { if (const auto photoId = std::get_if(&key)) { const auto photo = _session->data().photo(*photoId); return createThumb(key, photo); } else if (const auto msgId = std::get_if(&key)) { if (const auto item = _session->data().message(*msgId)) { if (const auto media = item->media()) { if (const auto photo = media->photo()) { return createThumb(key, photo); } else if (const auto document = media->document()) { return createThumb(key, document); } } } return createThumb(key, nullptr); } else if (const auto collageKey = std::get_if(&key)) { if (const auto itemId = std::get_if(&_context)) { if (const auto item = _session->data().message(*itemId)) { if (const auto media = item->media()) { if (const auto page = media->webpage()) { return createThumb( key, page->collage, collageKey->index); } } } } return createThumb(key, nullptr); } Unexpected("Value of Key in GroupThumbs::createThumb()"); } auto GroupThumbs::createThumb( Key key, const WebPageCollage &collage, int index) -> std::unique_ptr { if (index < 0 || index >= collage.items.size()) { return createThumb(key, nullptr); } const auto &item = collage.items[index]; if (const auto photo = std::get_if(&item)) { return createThumb(key, (*photo)); } else if (const auto document = std::get_if(&item)) { return createThumb(key, (*document)); } return createThumb(key, nullptr); } auto GroupThumbs::createThumb(Key key, std::nullptr_t) -> std::unique_ptr { const auto weak = base::make_weak(this); const auto origin = ComputeFileOrigin(key, _context); return std::make_unique(key, [=] { if (const auto strong = weak.get()) { strong->_activateStream.fire_copy(key); } }); } auto GroupThumbs::createThumb(Key key, not_null photo) -> std::unique_ptr { const auto weak = base::make_weak(this); const auto origin = ComputeFileOrigin(key, _context); return std::make_unique(key, photo, origin, [=] { if (const auto strong = weak.get()) { strong->_activateStream.fire_copy(key); } }); } auto GroupThumbs::createThumb(Key key, not_null document) -> std::unique_ptr { const auto weak = base::make_weak(this); const auto origin = ComputeFileOrigin(key, _context); return std::make_unique(key, document, origin, [=] { if (const auto strong = weak.get()) { strong->_activateStream.fire_copy(key); } }); } auto GroupThumbs::validateCacheEntry(Key key) -> not_null { const auto i = _cache.find(key); return (i != _cache.end()) ? i->second.get() : _cache.emplace(key, createThumb(key)).first->second.get(); } void GroupThumbs::markCacheStale() { _dying.clear(); for (const auto &cacheItem : _cache) { const auto &thumb = cacheItem.second; thumb->setState(Thumb::State::Unknown); } } void GroupThumbs::Refresh( not_null session, std::unique_ptr &instance, const SharedMediaWithLastSlice &slice, int index, int availableWidth) { RefreshFromSlice(session, instance, slice, index, availableWidth); } void GroupThumbs::Refresh( not_null session, std::unique_ptr &instance, const UserPhotosSlice &slice, int index, int availableWidth) { RefreshFromSlice(session, instance, slice, index, availableWidth); } void GroupThumbs::Refresh( not_null session, std::unique_ptr &instance, const CollageSlice &slice, int index, int availableWidth) { RefreshFromSlice(session, instance, slice, index, availableWidth); } void GroupThumbs::clear() { if (_items.empty()) { return; } base::take(_items); markCacheStale(); markRestAsDying(); startDelayedAnimation(); } void GroupThumbs::startDelayedAnimation() { _animation.stop(); _waitingForAnimationStart = true; countUpdatedRect(); } void GroupThumbs::resizeToWidth(int newWidth) { _width = newWidth; } int GroupThumbs::height() const { return st::mediaviewGroupPadding.top() + st::mediaviewGroupHeight + st::mediaviewGroupPadding.bottom(); } bool GroupThumbs::hiding() const { return _items.empty(); } bool GroupThumbs::hidden() const { return hiding() && !_waitingForAnimationStart && !_animation.animating(); } void GroupThumbs::checkForAnimationStart() { if (_waitingForAnimationStart) { _waitingForAnimationStart = false; _animation.start([=] { update(); }, 0., 1., kThumbDuration); } } void GroupThumbs::update() { if (_cache.empty()) { return; } _updateRequests.fire_copy(_updatedRect); } void GroupThumbs::paint(QPainter &p, int x, int y, int outerWidth) { const auto progress = _waitingForAnimationStart ? 0. : _animation.value(1.); x += (_width / 2); y += st::mediaviewGroupPadding.top(); auto initedCurrentIndex = int(_items.size()); for (auto i = _cache.begin(); i != _cache.end();) { const auto thumb = not_null{ i->second.get() }; const auto inited = thumb->inited(); thumb->paint(p, x, y, outerWidth, progress); if (thumb->removed()) { _dying.erase(ranges::remove(_dying, thumb), _dying.end()); i = _cache.erase(i); } else { if (!inited && thumb->inited() && thumb->state() == Thumb::State::Current) { initedCurrentIndex = ranges::find(_items, thumb) - begin(_items); } ++i; } } if (initedCurrentIndex < _items.size()) { animateAliveItems(initedCurrentIndex); } } ClickHandlerPtr GroupThumbs::getState(QPoint point) const { point -= QPoint((_width / 2), st::mediaviewGroupPadding.top()); for (const auto &cacheItem : _cache) { const auto &thumb = cacheItem.second; if (auto link = thumb->getState(point)) { return link; } } return nullptr; } void GroupThumbs::countUpdatedRect() { if (_cache.empty()) { return; } auto min = _width; auto max = 0; const auto left = [](const auto &cacheItem) { const auto &[key, thumb] = cacheItem; return thumb->leftToUpdate(); }; const auto right = [](const auto &cacheItem) { const auto &[key, thumb] = cacheItem; return thumb->rightToUpdate(); }; accumulate_min(min, left(*ranges::max_element( _cache, std::greater<>(), left))); accumulate_max(max, right(*ranges::max_element( _cache, std::less<>(), right))); _updatedRect = QRect( min, st::mediaviewGroupPadding.top(), max - min, st::mediaviewGroupHeight); } GroupThumbs::~GroupThumbs() = default; } // namespace View } // namespace Media