/* 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/info_flexible_scroll.h" #include "ui/effects/animation_value.h" #include "ui/widgets/scroll_area.h" #include "base/event_filter.h" #include "base/options.h" #include "styles/style_info.h" #include #include namespace Info { base::options::toggle AlternativeScrollProcessing({ .id = kAlternativeScrollProcessing, .name = "Use legacy scroll processing in profiles.", }); const char kAlternativeScrollProcessing[] = "alternative-scroll-processing"; FlexibleScrollHelper::FlexibleScrollHelper( not_null scroll, not_null inner, not_null pinnedToTop, Fn setPaintPadding, Fn>&&)> setViewport, FlexibleScrollData &data) : _scroll(scroll) , _inner(inner) , _pinnedToTop(pinnedToTop) , _setPaintPadding(setPaintPadding) , _setViewport(setViewport) , _data(data) { setupScrollAnimation(); if (AlternativeScrollProcessing.value()) { setupScrollHandling(); } else { setupScrollHandlingWithFilter(); } } void FlexibleScrollHelper::setupScrollAnimation() { constexpr auto kScrollStepTime = crl::time(260); const auto clearScrollState = [=] { _scrollAnimation.stop(); _scrollTopFrom = 0; _scrollTopTo = 0; _timeOffset = 0; _lastScrollApplied = 0; }; _scrollAnimation.init([=](crl::time now) { const auto progress = float64(now - _scrollAnimation.started() - _timeOffset) / kScrollStepTime; const auto eased = anim::easeOutQuint(1.0, progress); const auto scrollCurrent = anim::interpolate( _scrollTopFrom, _scrollTopTo, std::clamp(eased, 0., 1.)); scrollToY(scrollCurrent); _lastScrollApplied = scrollCurrent; if (progress >= 1) { clearScrollState(); } }); } void FlexibleScrollHelper::setupScrollHandling() { const auto heightDiff = [=] { return _pinnedToTop->maximumHeight() - _pinnedToTop->minimumHeight(); }; rpl::combine( _pinnedToTop->heightValue(), _inner->heightValue() ) | rpl::on_next([=](int, int h) { _data.contentHeightValue.fire(h + heightDiff()); }, _pinnedToTop->lifetime()); const auto singleStep = _scroll->verticalScrollBar()->singleStep() * QApplication::wheelScrollLines(); const auto step1 = (_pinnedToTop->maximumHeight() < st::infoProfileTopBarHeightMax) ? (st::infoProfileTopBarStep2 + st::lineWidth) : st::infoProfileTopBarStep1; const auto step2 = st::infoProfileTopBarStep2; // const auto stepDepreciation = singleStep // - st::infoProfileTopBarActionButtonsHeight; _scrollTopPrevious = _scroll->scrollTop(); _scroll->scrollTopValue( ) | rpl::on_next([=](int top) { if (_applyingFakeScrollState) { return; } const auto diff = top - _scrollTopPrevious; if (std::abs(diff) == singleStep) { const auto previousValue = top - diff; const auto nextStep = (diff > 0) ? ((previousValue == 0) ? step1 : (previousValue == step1) ? step2 : -1) // : ((top < step1 // && (top + stepDepreciation != step1 // || _scrollAnimation.animating())) : ((top < step1) ? 0 : (top < step2) ? step1 : -1); { _applyingFakeScrollState = true; scrollToY(previousValue); _applyingFakeScrollState = false; } if (_scrollAnimation.animating() && ((_scrollTopTo > _scrollTopFrom) != (diff > 0))) { auto overriddenDirection = true; if (_scrollTopTo > _scrollTopFrom) { // From going down to going up. if (_scrollTopTo == step1) { _scrollTopTo = 0; } else if (_scrollTopTo == step2) { _scrollTopTo = step1; } else { overriddenDirection = false; } } else { // From going up to going down. if (_scrollTopTo == 0) { _scrollTopTo = step1; } else if (_scrollTopTo == step1) { _scrollTopTo = step2; } else { overriddenDirection = false; } } if (overriddenDirection) { _timeOffset = crl::now() - _scrollAnimation.started(); _scrollTopFrom = _lastScrollApplied ? _lastScrollApplied : previousValue; return; } else { _scrollAnimation.stop(); _scrollTopFrom = 0; _scrollTopTo = 0; _timeOffset = 0; _lastScrollApplied = 0; } } _scrollTopFrom = _lastScrollApplied ? _lastScrollApplied : previousValue; if (!_scrollAnimation.animating()) { _scrollTopTo = ((nextStep != -1) ? nextStep : top); _scrollAnimation.start(); } else { if (_scrollTopTo > _scrollTopFrom) { // Down. if (_scrollTopTo == step1) { _scrollTopTo = step2; } else { _scrollTopTo += diff; } } else { // Up. if (_scrollTopTo == step2) { _scrollTopTo = step1; } else if (_scrollTopTo == step1) { _scrollTopTo = 0; } else { _scrollTopTo += diff; } } _timeOffset = (crl::now() - _scrollAnimation.started()); } return; } _scrollTopPrevious = top; const auto current = heightDiff() - top; _inner->moveToLeft(0, std::min(0, current)); _pinnedToTop->resize( _pinnedToTop->width(), std::max(current + _pinnedToTop->minimumHeight(), 0)); }, _inner->lifetime()); _data.fillerWidthValue.events( ) | rpl::on_next([=](int w) { _inner->resizeToWidth(w); }, _inner->lifetime()); _setPaintPadding({ 0, _pinnedToTop->minimumHeight(), 0, 0 }); _setViewport(_pinnedToTop->events( ) | rpl::filter([](not_null e) { return e->type() == QEvent::Wheel; })); } void FlexibleScrollHelper::setupScrollHandlingWithFilter() { rpl::combine( _pinnedToTop->heightValue(), _inner->heightValue() ) | rpl::on_next([=](int, int h) { const auto max = _pinnedToTop->maximumHeight(); const auto min = _pinnedToTop->minimumHeight(); const auto diff = max - min; const auto progress = (diff > 0) ? std::clamp( (_pinnedToTop->height() - min) / float64(diff), 0., 1.) : 1.; _data.contentHeightValue.fire(h + anim::interpolate(diff, 0, progress)); }, _pinnedToTop->lifetime()); const auto singleStep = _scroll->verticalScrollBar()->singleStep() * QApplication::wheelScrollLines(); const auto step1 = (_pinnedToTop->maximumHeight() < st::infoProfileTopBarHeightMax) ? (st::infoProfileTopBarStep2 + st::lineWidth) : st::infoProfileTopBarStep1; const auto step2 = st::infoProfileTopBarStep2; base::install_event_filter(_scroll->verticalScrollBar(), [=]( not_null e) { if (e->type() != QEvent::Wheel) { return base::EventFilterResult::Continue; } const auto wheel = static_cast(e.get()); const auto delta = wheel->angleDelta().y(); if (std::abs(delta) != 120) { scrollToY(_scroll->scrollTop() - delta); return base::EventFilterResult::Cancel; } const auto actualTop = _scroll->scrollTop(); const auto animationActive = _scrollAnimation.animating() && (_lastScrollApplied != _scrollTopTo); const auto top = animationActive ? (_lastScrollApplied ? _lastScrollApplied : actualTop) : actualTop; const auto diff = (delta > 0) ? -singleStep : singleStep; const auto previousValue = top; const auto targetTop = top + diff; const auto nextStep = (diff > 0) ? ((previousValue == 0) ? step1 : (previousValue == step1) ? step2 : -1) : ((targetTop < step1) ? 0 : (targetTop < step2) ? step1 : -1); if (animationActive && ((_scrollTopTo > _scrollTopFrom) != (diff > 0))) { auto overriddenDirection = true; if (_scrollTopTo > _scrollTopFrom) { if (_scrollTopTo == step1) { _scrollTopTo = 0; } else if (_scrollTopTo == step2) { _scrollTopTo = step1; } else { overriddenDirection = false; } } else { if (_scrollTopTo == 0) { _scrollTopTo = step1; } else if (_scrollTopTo == step1) { _scrollTopTo = step2; } else { overriddenDirection = false; } } if (overriddenDirection) { _timeOffset = crl::now() - _scrollAnimation.started(); _scrollTopFrom = _lastScrollApplied ? _lastScrollApplied : top; return base::EventFilterResult::Cancel; } else { _scrollAnimation.stop(); _scrollTopFrom = 0; _scrollTopTo = 0; _timeOffset = 0; _lastScrollApplied = 0; } } _scrollTopFrom = top; if (!animationActive) { _scrollTopTo = (nextStep != -1) ? nextStep : targetTop; _scrollAnimation.start(); } else { if (_scrollTopTo > _scrollTopFrom) { if (_scrollTopTo == step1) { _scrollTopTo = step2; } else { _scrollTopTo += diff; } } else { if (_scrollTopTo == step2) { _scrollTopTo = step1; } else if (_scrollTopTo == step1) { _scrollTopTo = 0; } else { _scrollTopTo += diff; } } _timeOffset = crl::now() - _scrollAnimation.started(); } return base::EventFilterResult::Cancel; }, _filterLifetime); _scroll->scrollTopValue() | rpl::on_next([=](int top) { applyScrollToPinnedLayout(top); }, _inner->lifetime()); _data.fillerWidthValue.events( ) | rpl::on_next([=](int w) { _inner->resizeToWidth(w); }, _inner->lifetime()); _setPaintPadding({ 0, _pinnedToTop->minimumHeight(), 0, 0 }); _setViewport(_pinnedToTop->events( ) | rpl::filter([](not_null e) { return e->type() == QEvent::Wheel; })); } void FlexibleScrollHelper::scrollToY(int scrollCurrent) { applyScrollToPinnedLayout(scrollCurrent); _scroll->scrollToY(scrollCurrent); } void FlexibleScrollHelper::applyScrollToPinnedLayout(int scrollCurrent) { const auto top = std::min(scrollCurrent, _scroll->scrollTopMax()); const auto minimumHeight = _pinnedToTop->minimumHeight(); const auto current = _pinnedToTop->maximumHeight() - minimumHeight - top; _inner->moveToLeft(0, std::min(0, current)); _pinnedToTop->resize( _pinnedToTop->width(), std::max(current + minimumHeight, 0)); } } // namespace Info