From 16128d61c0fd59f19b3fb00585d7421e40290f03 Mon Sep 17 00:00:00 2001 From: John Preston Date: Tue, 23 May 2023 20:13:02 +0400 Subject: [PATCH] Implement nice stories list scrolling. --- .../dialogs/dialogs_inner_widget.cpp | 45 +++++- .../dialogs/dialogs_inner_widget.h | 6 + .../SourceFiles/dialogs/dialogs_widget.cpp | 43 ++++-- Telegram/SourceFiles/dialogs/dialogs_widget.h | 3 +- .../dialogs/ui/dialogs_stories_content.cpp | 10 +- .../dialogs/ui/dialogs_stories_list.cpp | 142 +++++++++++++++++- .../dialogs/ui/dialogs_stories_list.h | 24 ++- .../window/window_session_controller.cpp | 4 +- 8 files changed, 248 insertions(+), 29 deletions(-) diff --git a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp index 5c29a8a36..a4c0bb267 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp +++ b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp @@ -142,7 +142,7 @@ InnerWidget::InnerWidget( , _stories(std::make_unique( this, Stories::ContentForSession(&controller->session()), - [=] { return st::dialogsStoriesFull.height - _visibleTop; })) + [=] { return _stories->height() - _visibleTop; })) , _shownList(controller->session().data().chatsList()->indexed()) , _st(&st::defaultDialogRow) , _pinnedShiftAnimation([=](crl::time now) { @@ -323,6 +323,18 @@ InnerWidget::InnerWidget( switchToFilter(filterId); }, lifetime()); + _stories->heightValue( + ) | rpl::filter([=] { + return (_viewportHeight > 0) && (defaultScrollTop() > _visibleTop); + }) | rpl::start_with_next([=] { + jumpToTop(); + }, lifetime()); + + _stories->entered( + ) | rpl::start_with_next([=] { + clearSelection(); + }, lifetime()); + handleChatListEntryRefreshes(); refreshWithCollapsedRows(true); @@ -428,6 +440,16 @@ int InnerWidget::dialogsOffset() const { - skipTopHeight(); } +rpl::producer<> InnerWidget::scrollToVeryTopRequests() const { + return _stories->expandRequests(); +} + +int InnerWidget::defaultScrollTop() const { + return storiesShown() + ? std::max(_stories->height() - st::dialogsStories.height, 0) + : 0; +} + int InnerWidget::fixedOnTopCount() const { auto result = 0; for (const auto &row : *_shownList) { @@ -1699,6 +1721,15 @@ void InnerWidget::mousePressReleased( } } +void InnerWidget::setViewportHeight(int viewportHeight) { + if (_viewportHeight != viewportHeight) { + _viewportHeight = viewportHeight; + if (height() < defaultScrollTop() + viewportHeight) { + refresh(); + } + } +} + void InnerWidget::setCollapsedPressed(int pressed) { if (_collapsedPressed != pressed) { if (_collapsedPressed >= 0) { @@ -2745,10 +2776,13 @@ void InnerWidget::refresh(bool toTop) { h = searchedOffset() + (_searchResults.size() * _st->height); } } + if (const auto storiesSkip = defaultScrollTop()) { + accumulate_max(h, storiesSkip + _viewportHeight); + } resize(width(), h); if (toTop) { stopReorderPinned(); - _mustScrollTo.fire({ 0, 0 }); + jumpToTop(); preloadRowsData(); } _controller->setDialogsListDisplayForced( @@ -3226,7 +3260,7 @@ void InnerWidget::switchToFilter(FilterId filterId) { filterId = 0; } if (_filterId == filterId) { - _mustScrollTo.fire({ 0, 0 }); + jumpToTop(); return; } saveChatsFilterScrollState(_filterId); @@ -3251,6 +3285,11 @@ void InnerWidget::switchToFilter(FilterId filterId) { } } +void InnerWidget::jumpToTop() { + const auto to = defaultScrollTop(); + _mustScrollTo.fire({ to, -1 }); +} + void InnerWidget::saveChatsFilterScrollState(FilterId filterId) { _chatsFilterScrollStates[filterId] = -y(); } diff --git a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.h b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.h index 56e71ec33..c7b2d899f 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.h +++ b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.h @@ -104,6 +104,10 @@ public: const QVector &my, const QVector &result); + [[nodiscard]] rpl::producer<> scrollToVeryTopRequests() const; + [[nodiscard]] int defaultScrollTop() const; + void setViewportHeight(int viewportHeight); + [[nodiscard]] FilterId filterId() const; void clearSelection(); @@ -279,6 +283,7 @@ private: int defaultRowTop(not_null row) const; void setupOnlineStatusCheck(); + void jumpToTop(); void updateRowCornerStatusShown(not_null history); void repaintDialogRowCornerStatus(not_null history); @@ -402,6 +407,7 @@ private: const not_null _controller; const std::unique_ptr _stories; + int _viewportHeight = 0; not_null _shownList; FilterId _filterId = 0; diff --git a/Telegram/SourceFiles/dialogs/dialogs_widget.cpp b/Telegram/SourceFiles/dialogs/dialogs_widget.cpp index dc8f3243e..24ff876e9 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_widget.cpp +++ b/Telegram/SourceFiles/dialogs/dialogs_widget.cpp @@ -260,6 +260,11 @@ Widget::Widget( } }, lifetime()); + _inner->scrollToVeryTopRequests( + ) | rpl::start_with_next([=] { + scrollToDefaultChecked(true); + }, lifetime()); + _inner->mustScrollTo( ) | rpl::start_with_next([=](const Ui::ScrollToRequest &data) { if (_scroll) { @@ -527,13 +532,15 @@ void Widget::setGeometryWithTopMoved( _topDelta = 0; } +void Widget::scrollToDefaultChecked(bool verytop) { + if (_scrollToAnimation.animating()) { + return; + } + scrollToDefault(verytop); +} + void Widget::setupScrollUpButton() { - _scrollToTop->setClickedCallback([=] { - if (_scrollToAnimation.animating()) { - return; - } - scrollToTop(); - }); + _scrollToTop->setClickedCallback([=] { scrollToDefaultChecked(); }); base::install_event_filter(_scrollToTop, [=](not_null event) { if (event->type() != QEvent::Wheel) { return base::EventFilterResult::Continue; @@ -1111,10 +1118,13 @@ void Widget::jumpToTop(bool belowPinned) { } } -void Widget::scrollToTop() { +void Widget::scrollToDefault(bool verytop) { _scrollToAnimation.stop(); auto scrollTop = _scroll->scrollTop(); - const auto scrollTo = 0; + const auto scrollTo = verytop ? 0 : _inner->defaultScrollTop(); + if (scrollTop <= scrollTo) { + return; + } const auto maxAnimatedDelta = _scroll->height(); if (scrollTo + maxAnimatedDelta < scrollTop) { scrollTop = scrollTo + maxAnimatedDelta; @@ -2494,7 +2504,11 @@ void Widget::updateControlsGeometry() { } auto scrollTop = forumReportTop + (_forumReportBar ? _forumReportBar->bar().height() : 0); - auto newScrollTop = _scroll->scrollTop() + _topDelta; + const auto wasScrollTop = _scroll->scrollTop(); + const auto newScrollTop = (_topDelta < 0 + && wasScrollTop <= _inner->defaultScrollTop()) + ? wasScrollTop + : (wasScrollTop + _topDelta); auto scrollHeight = height() - scrollTop; const auto putBottomButton = [&](auto &button) { if (button && !button->isHidden()) { @@ -2518,13 +2532,22 @@ void Widget::updateControlsGeometry() { const auto scrollw = _childList ? _narrowWidth : barw; const auto wasScrollHeight = _scroll->height(); + if (scrollHeight >= wasScrollHeight) { + _inner->setViewportHeight(scrollHeight); + } _scroll->setGeometry(0, scrollTop, scrollw, scrollHeight); + if (scrollHeight < wasScrollHeight) { + _inner->setViewportHeight(scrollHeight); + } _inner->resize(scrollw, _inner->height()); _inner->setNarrowRatio(narrowRatio); if (scrollHeight != wasScrollHeight) { controller()->floatPlayerAreaUpdated(); } - if (_topDelta) { + const auto startWithTop = _inner->defaultScrollTop(); + if (wasScrollHeight < startWithTop && scrollHeight >= startWithTop) { + _scroll->scrollToY(startWithTop); + } else if (newScrollTop != wasScrollTop) { _scroll->scrollToY(newScrollTop); } else { listScrollUpdated(); diff --git a/Telegram/SourceFiles/dialogs/dialogs_widget.h b/Telegram/SourceFiles/dialogs/dialogs_widget.h index 009f6ca3a..891afef3a 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_widget.h +++ b/Telegram/SourceFiles/dialogs/dialogs_widget.h @@ -209,7 +209,8 @@ private: mtpRequestId requestId); void peopleFailed(const MTP::Error &error, mtpRequestId requestId); - void scrollToTop(); + void scrollToDefault(bool verytop = false); + void scrollToDefaultChecked(bool verytop = false); void setupScrollUpButton(); void updateScrollUpVisibility(); void startScrollUpButtonAnimation(bool shown); diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_content.cpp b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_content.cpp index d4844a582..021d07328 100644 --- a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_content.cpp +++ b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_content.cpp @@ -173,10 +173,12 @@ rpl::producer ContentForSession(not_null session) { #if 0 // #TODO stories testing stories->allChanged() #endif - session->data().chatsListChanges( - ) | rpl::filter( - rpl::mappers::_1 == nullptr - ) | rpl::to_empty + rpl::merge( + session->data().chatsListChanges( + ) | rpl::filter( + rpl::mappers::_1 == nullptr + ) | rpl::to_empty, + session->data().unreadBadgeChanges()) ) | rpl::start_with_next([=] { consumer.put_next(state->next()); }, result); diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp index b87d4b5de..c4dfbbbd2 100644 --- a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp +++ b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp @@ -10,6 +10,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/painter.h" #include "styles/style_dialogs.h" +#include + namespace Dialogs::Stories { namespace { @@ -24,19 +26,31 @@ List::List( Fn shownHeight) : RpWidget(parent) , _shownHeight(shownHeight) { - resize(0, st::dialogsStoriesFull.height); + setCursor(style::cur_default); std::move(content) | rpl::start_with_next([=](Content &&content) { showContent(std::move(content)); }, lifetime()); + + _shownAnimation.stop(); + resize(0, _items.empty() ? 0 : st::dialogsStoriesFull.height); } void List::showContent(Content &&content) { if (_content == content) { return; } + if (content.users.empty()) { + _hidingItems = base::take(_items); + if (!_hidingItems.empty()) { + toggleAnimated(false); + } + return; + } + const auto hidden = _content.users.empty(); _content = std::move(content); - auto items = base::take(_items); + auto items = base::take(_items.empty() ? _hidingItems : _items); + _hidingItems.clear(); _items.reserve(_content.users.size()); for (const auto &user : _content.users) { const auto i = ranges::find(items, user.id, [](const Item &item) { @@ -53,10 +67,39 @@ void List::showContent(Content &&content) { item.user.name = user.name; item.nameCache = QImage(); } + item.user.unread = user.unread; } else { _items.emplace_back(Item{ .user = user }); } } + updateScrollMax(); + update(); + if (hidden) { + toggleAnimated(true); + } +} + +void List::toggleAnimated(bool shown) { + _shownAnimation.start( + [=] { updateHeight(); }, + shown ? 0. : 1., + shown ? 1. : 0., + st::slideWrapDuration); +} + +void List::updateHeight() { + const auto shown = _shownAnimation.value(_items.empty() ? 0. : 1.); + resize( + width(), + anim::interpolate(0, st::dialogsStoriesFull.height, shown)); +} + +void List::updateScrollMax() { + const auto &full = st::dialogsStoriesFull; + const auto singleFull = full.photoLeft * 2 + full.photo; + const auto widthFull = full.left + int(_items.size()) * singleFull; + _scrollLeftMax = std::max(widthFull - width(), 0); + _scrollLeft = std::clamp(_scrollLeft, 0, _scrollLeftMax); update(); } @@ -68,6 +111,18 @@ rpl::producer<> List::expandRequests() const { return _expandRequests.events(); } +rpl::producer<> List::entered() const { + return _entered.events(); +} + +void List::enterEventHook(QEnterEvent *e) { + _entered.fire({}); +} + +void List::resizeEvent(QResizeEvent *e) { + updateScrollMax(); +} + void List::paintEvent(QPaintEvent *e) { const auto &st = st::dialogsStories; const auto &full = st::dialogsStoriesFull; @@ -96,7 +151,7 @@ void List::paintEvent(QPaintEvent *e) { const auto startIndexFull = std::max(-leftFull, 0) / singleFull; const auto cellLeftFull = leftFull + (startIndexFull * singleFull); const auto endIndexFull = std::min( - (width() - cellLeftFull + singleFull - 1) / singleFull, + (width() - leftFull + singleFull - 1) / singleFull, itemsCount); const auto startIndexSmall = 0; const auto endIndexSmall = std::min(kSmallUserpicsShown, itemsCount); @@ -265,19 +320,92 @@ void List::paintEvent(QPaintEvent *e) { } void List::wheelEvent(QWheelEvent *e) { + const auto horizontal = (e->angleDelta().x() != 0); + if (!horizontal) { + e->ignore(); + return; + } + auto delta = horizontal + ? ((style::RightToLeft() ? -1 : 1) * (e->pixelDelta().x() + ? e->pixelDelta().x() + : e->angleDelta().x())) + : (e->pixelDelta().y() + ? e->pixelDelta().y() + : e->angleDelta().y()); -} - -void List::mouseMoveEvent(QMouseEvent *e) { - + const auto now = _scrollLeft; + const auto used = now - delta; + const auto next = std::clamp(used, 0, _scrollLeftMax); + if (next != now) { + _expandRequests.fire({}); + _scrollLeft = next; + //updateSelected(); + update(); + } + e->accept(); } void List::mousePressEvent(QMouseEvent *e) { + if (e->button() != Qt::LeftButton) { + return; + } + _mouseDownPosition = _lastMousePosition = e->globalPos(); + //updateSelected(); +} +void List::mouseMoveEvent(QMouseEvent *e) { + _lastMousePosition = e->globalPos(); + //updateSelected(); + + if (!_dragging && _mouseDownPosition) { + if ((_lastMousePosition - *_mouseDownPosition).manhattanLength() + >= QApplication::startDragDistance()) { + if (_shownHeight() < st::dialogsStoriesFull.height) { + _expandRequests.fire({}); + } + _dragging = true; + _startDraggingLeft = _scrollLeft; + } + } + checkDragging(); +} + +void List::checkDragging() { + if (_dragging) { + const auto sign = (style::RightToLeft() ? -1 : 1); + const auto newLeft = std::clamp( + (sign * (_mouseDownPosition->x() - _lastMousePosition.x()) + + _startDraggingLeft), + 0, + _scrollLeftMax); + if (newLeft != _scrollLeft) { + _scrollLeft = newLeft; + update(); + } + } } void List::mouseReleaseEvent(QMouseEvent *e) { + _lastMousePosition = e->globalPos(); + const auto guard = gsl::finally([&] { + _mouseDownPosition = std::nullopt; + }); + //const auto wasDown = std::exchange(_pressed, SpecialOver::None); + if (finishDragging()) { + return; + } + //updateSelected(); +} + +bool List::finishDragging() { + if (!_dragging) { + return false; + } + checkDragging(); + _dragging = false; + //updateSelected(); + return true; } } // namespace Dialogs::Stories diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.h b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.h index c0b80501e..d8b1bdbae 100644 --- a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.h +++ b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.h @@ -46,30 +46,48 @@ public: [[nodiscard]] rpl::producer clicks() const; [[nodiscard]] rpl::producer<> expandRequests() const; + [[nodiscard]] rpl::producer<> entered() const; private: struct Item { User user; - QImage frameSmall; - QImage frameFull; QImage nameCache; QColor nameCacheColor; bool subscribed = false; }; void showContent(Content &&content); + void enterEventHook(QEnterEvent *e) override; + void resizeEvent(QResizeEvent *e) override; void paintEvent(QPaintEvent *e) override; void wheelEvent(QWheelEvent *e) override; - void mouseMoveEvent(QMouseEvent *e) override; void mousePressEvent(QMouseEvent *e) override; + void mouseMoveEvent(QMouseEvent *e) override; void mouseReleaseEvent(QMouseEvent *e) override; + void updateScrollMax(); + void checkDragging(); + bool finishDragging(); + + void updateHeight(); + void toggleAnimated(bool shown); + Content _content; std::vector _items; + std::vector _hidingItems; Fn _shownHeight = 0; rpl::event_stream _clicks; rpl::event_stream<> _expandRequests; + rpl::event_stream<> _entered; + + Ui::Animations::Simple _shownAnimation; + + QPoint _lastMousePosition; + std::optional _mouseDownPosition; + int _startDraggingLeft = 0; int _scrollLeft = 0; + int _scrollLeftMax = 0; + bool _dragging = false; }; diff --git a/Telegram/SourceFiles/window/window_session_controller.cpp b/Telegram/SourceFiles/window/window_session_controller.cpp index 4e2563f79..34d9ca1be 100644 --- a/Telegram/SourceFiles/window/window_session_controller.cpp +++ b/Telegram/SourceFiles/window/window_session_controller.cpp @@ -1141,7 +1141,9 @@ void SessionController::openFolder(not_null folder) { if (_openedFolder.current() != folder) { resetFakeUnreadWhileOpened(); } - setActiveChatsFilter(0); + if (activeChatsFilterCurrent() != 0) { + setActiveChatsFilter(0); + } closeForum(); _openedFolder = folder.get(); }