diff --git a/Telegram/SourceFiles/history/view/history_view_subsection_tabs.cpp b/Telegram/SourceFiles/history/view/history_view_subsection_tabs.cpp index a0b99c395c..cdc024c3d5 100644 --- a/Telegram/SourceFiles/history/view/history_view_subsection_tabs.cpp +++ b/Telegram/SourceFiles/history/view/history_view_subsection_tabs.cpp @@ -16,21 +16,304 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_saved_sublist.h" #include "data/data_session.h" #include "data/data_thread.h" +#include "data/data_user.h" #include "dialogs/dialogs_main_list.h" #include "history/history.h" #include "lang/lang_keys.h" +#include "main/main_session.h" +#include "ui/effects/ripple_animation.h" #include "ui/text/text_utilities.h" #include "ui/widgets/buttons.h" #include "ui/widgets/discrete_sliders.h" #include "ui/widgets/scroll_area.h" #include "ui/widgets/shadow.h" +#include "ui/dynamic_image.h" +#include "ui/dynamic_thumbnails.h" #include "window/window_session_controller.h" #include "styles/style_chat.h" namespace HistoryView { namespace { -constexpr auto kDefaultLimit = 5;AssertIsDebug()// 10; +constexpr auto kDefaultLimit = 5; AssertIsDebug()// 10; +constexpr auto kMaxNameLines = 3; + +class VerticalSlider final : public Ui::RpWidget { +public: + explicit VerticalSlider(not_null parent); + + struct Section { + std::shared_ptr userpic; + QString text; + }; + + void setSections(std::vector
sections, Fn paused); + void setActiveSectionFast(int active); + + void fitHeightToSections(); + + [[nodiscard]] rpl::producer sectionActivated() const { + return _sectionActivated.events(); + } + + [[nodiscard]] int sectionsCount() const; + [[nodiscard]] int lookupSectionTop(int index) const; + +private: + struct Tab { + std::shared_ptr userpic; + Ui::Text::String text; + std::unique_ptr ripple; + int top = 0; + int height = 0; + bool subscribed = false; + }; + struct Range { + int top = 0; + int height = 0; + }; + + void paintEvent(QPaintEvent *e) override; + void timerEvent(QTimerEvent *e) override; + void mousePressEvent(QMouseEvent *e) override; + void mouseReleaseEvent(QMouseEvent *e) override; + + void startRipple(int index); + [[nodiscard]] int getIndexFromPosition(QPoint position) const; + [[nodiscard]] QImage prepareRippleMask(int index, const Tab &tab); + + void activateCallback(); + [[nodiscard]] Range getFinalActiveRange() const; + + const style::ChatTabsVertical &_st; + Ui::RoundRect _bar; + std::vector _tabs; + int _active = -1; + int _pressed = -1; + Ui::Animations::Simple _activeTop; + Ui::Animations::Simple _activeHeight; + + int _timerId = -1; + crl::time _callbackAfterMs = 0; + + rpl::event_stream _sectionActivated; + Fn _paused; + +}; + +VerticalSlider::VerticalSlider(not_null parent) +: RpWidget(parent) +, _st(st::chatTabsVertical) +, _bar(_st.barRadius, _st.barFg) { + setCursor(style::cur_pointer); +} + +void VerticalSlider::setSections( + std::vector
sections, + Fn paused) { + auto old = base::take(_tabs); + _tabs.reserve(sections.size()); + + for (auto §ion : sections) { + const auto i = ranges::find(old, section.userpic, &Tab::userpic); + if (i != end(old)) { + _tabs.push_back(std::move(*i)); + old.erase(i); + } else { + _tabs.push_back({ .userpic = std::move(section.userpic), }); + } + _tabs.back().text = Ui::Text::String( + _st.nameStyle, + section.text, + kDefaultTextOptions, + _st.nameWidth); + } + for (const auto &was : old) { + if (was.subscribed) { + was.userpic->subscribeToUpdates(nullptr); + } + } +} + +void VerticalSlider::setActiveSectionFast(int active) { + _active = active; + _activeTop.stop(); + _activeHeight.stop(); +} + +void VerticalSlider::fitHeightToSections() { + auto top = 0; + for (auto &tab : _tabs) { + tab.top = top; + tab.height = _st.baseHeight + std::min( + _st.nameStyle.font->height * kMaxNameLines, + tab.text.countHeight(_st.nameWidth, true)); + top += tab.height; + } + resize(_st.width, top); +} + +int VerticalSlider::sectionsCount() const { + return int(_tabs.size()); +} + +int VerticalSlider::lookupSectionTop(int index) const { + Expects(index >= 0 && index < _tabs.size()); + + return _tabs[index].top; +} + +VerticalSlider::Range VerticalSlider::getFinalActiveRange() const { + return (_active >= 0) + ? Range{ _tabs[_active].top, _tabs[_active].height } + : Range(); +} + +void VerticalSlider::paintEvent(QPaintEvent *e) { + const auto finalRange = getFinalActiveRange(); + const auto range = Range{ + int(base::SafeRound(_activeTop.value(finalRange.top))), + int(base::SafeRound(_activeHeight.value(finalRange.height))), + }; + + auto p = QPainter(this); + auto clip = e->rect(); + const auto drawRect = [&](QRect rect) { + _bar.paint(p, rect); + }; + const auto nameLeft = (_st.width - _st.nameWidth) / 2; + for (auto &tab : _tabs) { + if (!clip.intersects(QRect(0, tab.top, width(), tab.height))) { + continue; + } + const auto divider = std::max(std::min(tab.height, range.height), 1); + const auto active = 1. + - std::clamp( + std::abs(range.top - tab.top) / float64(divider), + 0., + 1.); + if (tab.ripple) { + const auto color = anim::color( + _st.rippleBg, + _st.rippleBgActive, + active); + tab.ripple->paint(p, 0, tab.top, width(), &color); + if (tab.ripple->empty()) { + tab.ripple.reset(); + } + } + + if (!tab.subscribed) { + tab.subscribed = true; + tab.userpic->subscribeToUpdates([=] { update(); }); + } + const auto &image = tab.userpic->image(_st.userpicSize); + const auto userpicLeft = (width() - _st.userpicSize) / 2; + p.drawImage(userpicLeft, tab.top + _st.userpicTop, image); + p.setPen(anim::pen(_st.nameFg, _st.nameFgActive, active)); + tab.text.draw(p, { + .position = QPoint(nameLeft, tab.top + _st.nameTop), + .outerWidth = width(), + .availableWidth = _st.nameWidth, + .align = style::al_top, + .paused = _paused && _paused(), + }); + } + if (range.height > 0) { + const auto add = _st.barStroke / 2; + drawRect(myrtlrect(-add, range.top, _st.barStroke, range.height)); + } +} + +void VerticalSlider::timerEvent(QTimerEvent *e) { + activateCallback(); +} + +void VerticalSlider::startRipple(int index) { + if (!_st.ripple.showDuration) { + return; + } + auto &tab = _tabs[index]; + if (!tab.ripple) { + auto mask = prepareRippleMask(index, tab); + tab.ripple = std::make_unique( + _st.ripple, + std::move(mask), + [this] { update(); }); + } + const auto point = mapFromGlobal(QCursor::pos()); + tab.ripple->add(point - QPoint(0, tab.top)); +} + +QImage VerticalSlider::prepareRippleMask(int index, const Tab &tab) { + return Ui::RippleAnimation::RectMask(QSize(width(), tab.height)); +} + +int VerticalSlider::getIndexFromPosition(QPoint position) const { + const auto count = int(_tabs.size()); + for (auto i = 0; i != count; ++i) { + const auto &tab = _tabs[i]; + if (position.y() < tab.top + tab.height) { + return i; + } + } + return count - 1; +} + +void VerticalSlider::mousePressEvent(QMouseEvent *e) { + for (auto i = 0, count = int(_tabs.size()); i != count; ++i) { + auto &tab = _tabs[i]; + if (tab.top <= e->y() && e->y() < tab.top + tab.height) { + startRipple(i); + _pressed = i; + break; + } + } +} + +void VerticalSlider::mouseReleaseEvent(QMouseEvent *e) { + const auto pressed = std::exchange(_pressed, -1); + if (pressed < 0) { + return; + } + + const auto index = getIndexFromPosition(e->pos()); + if (pressed < _tabs.size()) { + if (_tabs[pressed].ripple) { + _tabs[pressed].ripple->lastStop(); + } + } + if (index == pressed) { + if (_active != index) { + _callbackAfterMs = crl::now() + _st.duration; + activateCallback(); + + const auto from = getFinalActiveRange(); + _active = index; + const auto to = getFinalActiveRange(); + const auto updater = [this] { update(); }; + _activeTop.start(updater, from.top, to.top, _st.duration); + _activeHeight.start( + updater, + from.height, + to.height, + _st.duration); + } + } +} + +void VerticalSlider::activateCallback() { + if (_timerId >= 0) { + killTimer(_timerId); + _timerId = -1; + } + auto ms = crl::now(); + if (ms >= _callbackAfterMs) { + _sectionActivated.fire_copy(_active); + } else { + _timerId = startTimer(_callbackAfterMs - ms, Qt::PreciseTimer); + } +} } // namespace @@ -47,6 +330,13 @@ SubsectionTabs::SubsectionTabs( track(); refreshSlice(); setupHorizontal(parent); + + dataChanged() | rpl::start_with_next([=] { + if (_loading) { + _loading = false; + refreshSlice(); + } + }, _lifetime); } SubsectionTabs::~SubsectionTabs() { @@ -63,6 +353,8 @@ void SubsectionTabs::setupHorizontal(not_null parent) { if (!_shadow) { _shadow = Ui::CreateChild(parent); _shadow->show(); + } else { + _shadow->raise(); } const auto toggle = Ui::CreateChild( @@ -132,13 +424,6 @@ void SubsectionTabs::setupHorizontal(not_null parent) { } }, _horizontal->lifetime()); - dataChanged() | rpl::start_with_next([=] { - if (_loading) { - _loading = false; - refreshSlice(); - } - }, _horizontal->lifetime()); - _horizontal->sizeValue( ) | rpl::start_with_next([=](QSize size) { const auto togglew = toggle->width(); @@ -147,7 +432,11 @@ void SubsectionTabs::setupHorizontal(not_null parent) { }, scroll->lifetime()); _horizontal->paintRequest() | rpl::start_with_next([=](QRect clip) { - QPainter(_horizontal).fillRect(clip, st::windowBg); + QPainter(_horizontal).fillRect( + clip.intersected( + _horizontal->rect().marginsRemoved( + { 0, 0, 0, st::lineWidth })), + st::windowBg); }, _horizontal->lifetime()); _refreshed.events_starting_with_copy( @@ -254,8 +543,52 @@ void SubsectionTabs::setupVertical(not_null parent) { toggleModes(); }); toggle->move(0, 0); - const auto scroll = Ui::CreateChild(_vertical); + const auto scroll = Ui::CreateChild( + _vertical, + st::chatTabsScroll); scroll->show(); + const auto tabs = scroll->setOwnedWidget( + object_ptr(scroll)); + tabs->sectionActivated() | rpl::start_with_next([=](int active) { + if (active >= 0 + && active < _slice.size() + && _active != _slice[active]) { + auto params = Window::SectionShow(); + params.way = Window::SectionShow::Way::ClearStack; + params.animated = anim::type::instant; + _controller->showThread(_slice[active], {}, params); + } + }, tabs->lifetime()); + + rpl::merge( + scroll->scrolls(), + _scrollCheckRequests.events(), + scroll->heightValue() | rpl::skip(1) | rpl::map_to(rpl::empty) + ) | rpl::start_with_next([=] { + const auto height = scroll->height(); + const auto top = scroll->scrollTop(); + const auto max = scroll->scrollTopMax(); + const auto availableTop = top; + const auto availableBottom = (max - top); + if (max <= 2 * height && _afterAvailable > 0) { + _beforeLimit *= 2; + _afterLimit *= 2; + } + if (availableTop < height + && _beforeSkipped.value_or(0) > 0 + && !_slice.empty()) { + _around = _slice.front(); + refreshSlice(); + } else if (availableBottom < height) { + if (_afterAvailable > 0) { + _around = _slice.back(); + refreshSlice(); + } else if (!_afterSkipped.has_value()) { + _loading = true; + loadMore(); + } + } + }, _vertical->lifetime()); _vertical->sizeValue( ) | rpl::start_with_next([=](QSize size) { @@ -271,7 +604,90 @@ void SubsectionTabs::setupVertical(not_null parent) { _refreshed.events_starting_with_copy( rpl::empty ) | rpl::start_with_next([=] { - _vertical->resize(std::max(toggle->width(), 0), 0); + auto sections = std::vector(); + auto activeIndex = -1; + for (const auto &thread : _slice) { + if (thread == _active) { + activeIndex = int(sections.size()); + } + if (const auto topic = thread->asTopic()) { + sections.push_back({ + .userpic = (topic->iconId() + ? Ui::MakeEmojiThumbnail( + &topic->owner(), + Data::SerializeCustomEmojiId(topic->iconId())) + : Ui::MakeUserpicThumbnail( + _controller->session().user())), + .text = topic->title(), + }); + } else if (const auto sublist = thread->asSublist()) { + const auto peer = sublist->sublistPeer(); + sections.push_back({ + .userpic = Ui::MakeUserpicThumbnail(peer), + .text = peer->shortName(), + }); + } else { + sections.push_back({ + .userpic = Ui::MakeUserpicThumbnail( + _controller->session().user()), + .text = tr::lng_filters_all_short(tr::now), + }); + } + } + const auto paused = [=] { + return _controller->isGifPausedAtLeastFor( + Window::GifPauseReason::Any); + }; + + auto scrollSavingThread = (Data::Thread*)nullptr; + auto scrollSavingShift = 0; + auto scrollSavingIndex = -1; + if (const auto count = tabs->sectionsCount()) { + const auto scrollTop = scroll->scrollTop(); + auto indexTop = tabs->lookupSectionTop(0); + for (auto index = 0; index != count; ++index) { + const auto nextTop = (index + 1 != count) + ? tabs->lookupSectionTop(index + 1) + : (indexTop + scrollTop + 1); + if (indexTop <= scrollTop && nextTop > scrollTop) { + scrollSavingThread = _sectionsSlice[index]; + scrollSavingShift = scrollTop - indexTop; + break; + } + indexTop = nextTop; + } + scrollSavingIndex = scrollSavingThread + ? int(ranges::find(_slice, not_null(scrollSavingThread)) + - begin(_slice)) + : -1; + if (scrollSavingIndex == _slice.size()) { + scrollSavingIndex = -1; + for (auto index = 0; index != count; ++index) { + const auto thread = _sectionsSlice[index]; + if (ranges::contains(_slice, thread)) { + scrollSavingThread = thread; + scrollSavingShift = scrollTop + - tabs->lookupSectionTop(index); + scrollSavingIndex = index; + break; + } + } + } + } + + tabs->setSections(sections, paused); + tabs->fitHeightToSections(); + tabs->setActiveSectionFast(activeIndex); + _sectionsSlice = _slice; + _vertical->resize( + std::max(toggle->width(), tabs->width()), + _vertical->height()); + if (scrollSavingIndex >= 0) { + scroll->scrollToY(tabs->lookupSectionTop(scrollSavingIndex) + + scrollSavingShift); + } + + _scrollCheckRequests.fire({}); }, _vertical->lifetime()); } @@ -341,7 +757,7 @@ void SubsectionTabs::setBoundingRect(QRect boundingRect) { _horizontal->height()); _shadow->setGeometry( boundingRect.x(), - _horizontal->y() + _horizontal->height(), + _horizontal->y() + _horizontal->height() - st::lineWidth, boundingRect.width(), st::lineWidth); } else { @@ -367,7 +783,7 @@ int SubsectionTabs::leftSkip() const { } int SubsectionTabs::topSkip() const { - return _horizontal ? _horizontal->height() : 0; + return _horizontal ? (_horizontal->height() - st::lineWidth) : 0; } void SubsectionTabs::raise() { diff --git a/Telegram/SourceFiles/ui/chat/chat.style b/Telegram/SourceFiles/ui/chat/chat.style index 76b725da93..6de023c332 100644 --- a/Telegram/SourceFiles/ui/chat/chat.style +++ b/Telegram/SourceFiles/ui/chat/chat.style @@ -1284,3 +1284,43 @@ chatTabsSlider: SettingsSlider(defaultSettingsSlider) { rippleBgActive: lightButtonBgOver; ripple: defaultRippleAnimation; } + +ChatTabsVertical { + barStroke: pixels; + barRadius: pixels; + barFg: color; + nameStyle: TextStyle; + nameWidth: pixels; + nameTop: pixels; + nameFg: color; + nameFgActive: color; + userpicTop: pixels; + userpicSize: pixels; + baseHeight: pixels; + width: pixels; + ripple: RippleAnimation; + rippleBg: color; + rippleBgActive: color; + duration: int; +} + +chatTabsVertical: ChatTabsVertical { + barStroke: 8px; + barRadius: 4px; + barFg: sliderBgActive; + nameStyle: TextStyle(defaultTextStyle) { + font: font(10px); + } + nameWidth: 46px; + nameTop: 46px; + nameFg: windowSubTextFg; + nameFgActive: lightButtonFg; + userpicTop: 8px; + userpicSize: 36px; + baseHeight: 56px; + width: 56px; + ripple: defaultRippleAnimation; + rippleBg: windowBgOver; + rippleBgActive: lightButtonBgOver; + duration: 150; +}