Implement nice stories list scrolling.

This commit is contained in:
John Preston 2023-05-23 20:13:02 +04:00
parent 1d27c8c940
commit 16128d61c0
8 changed files with 248 additions and 29 deletions

View File

@ -142,7 +142,7 @@ InnerWidget::InnerWidget(
, _stories(std::make_unique<Stories::List>(
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();
}

View File

@ -104,6 +104,10 @@ public:
const QVector<MTPPeer> &my,
const QVector<MTPPeer> &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*> row) const;
void setupOnlineStatusCheck();
void jumpToTop();
void updateRowCornerStatusShown(not_null<History*> history);
void repaintDialogRowCornerStatus(not_null<History*> history);
@ -402,6 +407,7 @@ private:
const not_null<Window::SessionController*> _controller;
const std::unique_ptr<Stories::List> _stories;
int _viewportHeight = 0;
not_null<IndexedList*> _shownList;
FilterId _filterId = 0;

View File

@ -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<QEvent*> 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();

View File

@ -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);

View File

@ -173,10 +173,12 @@ rpl::producer<Content> ContentForSession(not_null<Main::Session*> 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);

View File

@ -10,6 +10,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "ui/painter.h"
#include "styles/style_dialogs.h"
#include <QtWidgets/QApplication>
namespace Dialogs::Stories {
namespace {
@ -24,19 +26,31 @@ List::List(
Fn<int()> 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

View File

@ -46,30 +46,48 @@ public:
[[nodiscard]] rpl::producer<uint64> 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<Item> _items;
std::vector<Item> _hidingItems;
Fn<int()> _shownHeight = 0;
rpl::event_stream<uint64> _clicks;
rpl::event_stream<> _expandRequests;
rpl::event_stream<> _entered;
Ui::Animations::Simple _shownAnimation;
QPoint _lastMousePosition;
std::optional<QPoint> _mouseDownPosition;
int _startDraggingLeft = 0;
int _scrollLeft = 0;
int _scrollLeftMax = 0;
bool _dragging = false;
};

View File

@ -1141,7 +1141,9 @@ void SessionController::openFolder(not_null<Data::Folder*> folder) {
if (_openedFolder.current() != folder) {
resetFakeUnreadWhileOpened();
}
setActiveChatsFilter(0);
if (activeChatsFilterCurrent() != 0) {
setActiveChatsFilter(0);
}
closeForum();
_openedFolder = folder.get();
}