/* 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 "info/media/info_media_grid_zoom.h" #include "info/media/info_media_list_widget.h" #include "info/media/info_media_list_section.h" #include "core/application.h" #include "core/core_settings.h" #include "ui/ui_utility.h" #include "styles/style_info.h" #include "styles/style_layers.h" #include "styles/style_overview.h" namespace Info::Media { namespace { constexpr auto kStepMin = -2; constexpr auto kStepMax = 3; constexpr auto kPinchStep = 0.5; constexpr auto kFadeDuration = crl::time(200); } // namespace ListZoom::ListZoom(not_null list) : _list(list) , _step(std::clamp( Core::App().settings().mediaGridZoomStep(), kStepMin, kStepMax)) { } bool ListZoom::isZoomable() const { switch (_list->_provider->type()) { case Type::Photo: case Type::Video: case Type::PhotoVideo: case Type::RoundFile: return true; default: return false; } } int ListZoom::minVideoGridSize() const { return 2 * st::overviewVideoStatusMargin + 3 * st::overviewVideoStatusPadding.x() + st::overviewVideoPlay.width() + st::normalFont->width(u"00:00"_q); } int ListZoom::minGridSizeForStep(int step) const { const auto base = st::infoMediaMinGridSize + step * st::infoMediaGridSizeStep; switch (_list->_provider->type()) { case Type::Video: case Type::PhotoVideo: case Type::RoundFile: return std::max(base, minVideoGridSize()); default: return base; } } int ListZoom::minGridSize() const { return minGridSizeForStep(_step); } int ListZoom::columnsForGridSize(int gridSize) const { const auto outer = _list->width(); if (outer <= 0) { return 1; } const auto skip = st::infoMediaSkip; const auto left = st::infoMediaLeft; return std::max(1, (outer - left * 2 + skip) / (gridSize + skip)); } std::optional ListZoom::nextZoomStep(int direction) const { if (_list->_sections.empty() || !isZoomable()) { return std::nullopt; } const auto currentColumns = columnsForGridSize(minGridSize()); for (auto step = _step + direction ; step >= kStepMin && step <= kStepMax ; step += direction) { const auto columns = columnsForGridSize(minGridSizeForStep(step)); if ((direction > 0) ? (columns < currentColumns) : (columns > currentColumns)) { return step; } } return std::nullopt; } QPoint ListZoom::defaultAnchor() const { return QPoint( _list->width() / 2, (_list->_visibleTop + _list->_visibleBottom) / 2); } void ListZoom::zoomBy(int direction, QPoint anchor) { if (const auto step = nextZoomStep(direction)) { apply(*step, anchor); } } void ListZoom::zoomIn() { zoomBy(1, defaultAnchor()); } void ListZoom::zoomOut() { zoomBy(-1, defaultAnchor()); } bool ListZoom::canZoomIn() const { return nextZoomStep(1).has_value(); } bool ListZoom::canZoomOut() const { return nextZoomStep(-1).has_value(); } QPixmap ListZoom::grabViewport(int height) { const auto width = _list->width(); if (width <= 0 || height <= 0) { return QPixmap(); } return Ui::GrabWidget( _list.get(), QRect(0, _list->_visibleTop, width, height), st::boxBg->c); } void ListZoom::apply(int step, QPoint anchor) { step = std::clamp(step, kStepMin, kStepMax); if (step == _step || _list->_sections.empty()) { return; } const auto oldColumns = columnsForGridSize(minGridSize()); const auto newColumns = columnsForGridSize(minGridSizeForStep(step)); _step = step; Core::App().settings().setMediaGridZoomStep(step); Core::App().saveSettingsDelayed(); if (newColumns == oldColumns) { return; } const auto fadeHeight = _list->_visibleBottom - _list->_visibleTop; const auto anchorScreen = QPoint( anchor.x(), anchor.y() - _list->_visibleTop); _fade.stop(); auto oldPix = grabViewport(fadeHeight); _list->_scrollTopState = _list->countScrollState(anchor); _list->resizeToWidth(_list->width()); _list->restoreScrollState(); auto newPix = grabViewport(fadeHeight); if (!oldPix.isNull() && !newPix.isNull() && fadeHeight > 0) { _oldPix = std::move(oldPix); _newPix = std::move(newPix); _anchorScreen = anchorScreen; _fadeHeight = fadeHeight; _scaleRatio = float64(oldColumns) / newColumns; _fade.start([=] { _list->update( QRect(0, _list->_visibleTop, _list->width(), _fadeHeight)); }, 0., 1., kFadeDuration, anim::easeOutCirc); } _list->update(); } bool ListZoom::handleNativeGesture(not_null e) { if (e->gestureType() == Qt::BeginNativeGesture) { _pinchAccumulated = 0.; return false; } else if (e->gestureType() != Qt::ZoomNativeGesture) { return false; } else if (!isZoomable()) { return false; } const auto anchor = e->pos(); _pinchAccumulated += e->value(); while (_pinchAccumulated >= kPinchStep) { _pinchAccumulated -= kPinchStep; zoomBy(1, anchor); } while (_pinchAccumulated <= -kPinchStep) { _pinchAccumulated += kPinchStep; zoomBy(-1, anchor); } return true; } bool ListZoom::processWheel(not_null e) { if (!isZoomable()) { return false; } const auto delta = e->angleDelta().y(); if (!delta) { return false; } const auto position = e->position(); const auto anchor = QPoint( qRound(position.x()), _list->_visibleTop + qRound(position.y())); zoomBy((delta > 0) ? 1 : -1, anchor); return true; } bool ListZoom::paint(QPainter &p) { if (_fade.animating() && !_oldPix.isNull() && !_newPix.isNull()) { const auto progress = _fade.value(1.); const auto top = _list->_visibleTop; const auto width = _list->width(); const auto target = QRect(0, top, width, _fadeHeight); const auto pivot = QPointF( _anchorScreen.x(), top + _anchorScreen.y()); p.setRenderHint(QPainter::SmoothPixmapTransform); const auto layer = [&]( const QPixmap &pix, float64 scale, float64 opacity) { if (opacity <= 0.) { return; } p.save(); p.setOpacity(opacity); p.translate(pivot); p.scale(scale, scale); p.translate(-pivot); p.drawPixmap(target, pix); p.restore(); }; const auto inv = 1. / _scaleRatio; layer(_newPix, inv + (1. - inv) * progress, progress); layer(_oldPix, 1. + (_scaleRatio - 1.) * progress, 1. - progress); return true; } else if (!_oldPix.isNull() || !_newPix.isNull()) { _oldPix = QPixmap(); _newPix = QPixmap(); } return false; } } // namespace Info::Media