From 3e413a036fd33e59609e0c8c949301e4ca5f4e93 Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Mon, 4 Nov 2024 20:32:24 +0300 Subject: [PATCH] Added initial implementation of class for reorder of tabs slider. --- .../chat_filters_tabs_slider_reorder.cpp | 373 ++++++++++++++++++ .../chat_filters_tabs_slider_reorder.h | 98 +++++ Telegram/cmake/td_ui.cmake | 2 + Telegram/lib_base | 2 +- 4 files changed, 474 insertions(+), 1 deletion(-) create mode 100644 Telegram/SourceFiles/ui/widgets/chat_filters_tabs_slider_reorder.cpp create mode 100644 Telegram/SourceFiles/ui/widgets/chat_filters_tabs_slider_reorder.h diff --git a/Telegram/SourceFiles/ui/widgets/chat_filters_tabs_slider_reorder.cpp b/Telegram/SourceFiles/ui/widgets/chat_filters_tabs_slider_reorder.cpp new file mode 100644 index 000000000..8695aac32 --- /dev/null +++ b/Telegram/SourceFiles/ui/widgets/chat_filters_tabs_slider_reorder.cpp @@ -0,0 +1,373 @@ +/* +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 "ui/widgets/chat_filters_tabs_slider_reorder.h" + +#include "ui/widgets/scroll_area.h" +#include "styles/style_basic.h" + +#include +#include +#include + +namespace Ui { +namespace { + +constexpr auto kScrollFactor = 0.05; + +} // namespace + +ChatsFiltersTabsReorder::ChatsFiltersTabsReorder( + not_null layout, + not_null scroll) +: _layout(layout) +, _scroll(scroll) +, _scrollAnimation([this] { updateScrollCallback(); }) { +} + +ChatsFiltersTabsReorder::ChatsFiltersTabsReorder( + not_null layout) +: _layout(layout) { +} + +void ChatsFiltersTabsReorder::cancel() { + if (_currentWidget) { + cancelCurrent(indexOf(_currentWidget)); + } + _lifetime.destroy(); + for (auto i = 0, count = _layout->count(); i != count; ++i) { + _layout->setHorizontalShift(i, 0); + } + _entries.clear(); +} + +void ChatsFiltersTabsReorder::start() { + const auto count = _layout->count(); + if (count < 2) { + return; + } + _layout->events() + | rpl::start_with_next_done([this](not_null e) { + switch (e->type()) { + case QEvent::MouseMove: + mouseMove(static_cast(e.get())->globalPos()); + break; + case QEvent::MouseButtonPress: { + const auto m = static_cast(e.get()); + mousePress(m->button(), m->pos(), m->globalPos()); + break; + } + case QEvent::MouseButtonRelease: + mouseRelease(static_cast(e.get())->button()); + break; + } + }, [this] { + cancel(); + }, _lifetime); + + for (auto i = 0; i != count; ++i) { + const auto widget = _layout->widgetAt(i); + _entries.push_back({ widget }); + } +} + +void ChatsFiltersTabsReorder::addPinnedInterval(int from, int length) { + _pinnedIntervals.push_back({ from, length }); +} + +void ChatsFiltersTabsReorder::clearPinnedIntervals() { + _pinnedIntervals.clear(); +} + +bool ChatsFiltersTabsReorder::Interval::isIn(int index) const { + return (index >= from) && (index < (from + length)); +} + +bool ChatsFiltersTabsReorder::isIndexPinned(int index) const { + return ranges::any_of(_pinnedIntervals, [&](const Interval &i) { + return i.isIn(index); + }); +} + +void ChatsFiltersTabsReorder::checkForStart(QPoint position) { + const auto shift = position.x() - _currentStart; + const auto delta = QApplication::startDragDistance(); + if (std::abs(shift) <= delta) { + return; + } + _currentState = State::Started; + _currentStart += (shift > 0) ? delta : -delta; + + const auto index = indexOf(_currentWidget); + _layout->setRaised(index); + _currentDesiredIndex = index; + _updates.fire({ _currentWidget, index, index, _currentState }); + + updateOrder(index, position); +} + +void ChatsFiltersTabsReorder::updateOrder(int index, QPoint position) { + if (isIndexPinned(index)) { + return; + } + const auto shift = position.x() - _currentStart; + auto ¤t = _entries[index]; + current.shiftAnimation.stop(); + current.shift = current.finalShift = shift; + _layout->setHorizontalShift(index, shift); + + checkForScrollAnimation(); + + const auto count = _entries.size(); + const auto currentWidth = current.widget->width; + const auto currentMiddle = current.widget->left + + shift + + currentWidth / 2; + _currentDesiredIndex = index; + if (shift > 0) { + for (auto next = index + 1; next != count; ++next) { + if (isIndexPinned(next)) { + return; + } + const auto &e = _entries[next]; + if (currentMiddle < e.widget->left + e.widget->width / 2) { + moveToShift(next, 0); + } else { + _currentDesiredIndex = next; + moveToShift(next, -currentWidth); + } + } + for (auto prev = index - 1; prev >= 0; --prev) { + moveToShift(prev, 0); + } + } else { + for (auto next = index + 1; next != count; ++next) { + moveToShift(next, 0); + } + for (auto prev = index - 1; prev >= 0; --prev) { + if (isIndexPinned(prev)) { + return; + } + const auto &e = _entries[prev]; + if (currentMiddle >= e.widget->left + e.widget->width / 2) { + moveToShift(prev, 0); + } else { + _currentDesiredIndex = prev; + moveToShift(prev, currentWidth); + } + } + } +} + +void ChatsFiltersTabsReorder::mousePress( + Qt::MouseButton button, + QPoint position, + QPoint globalPosition) { + if (button != Qt::LeftButton) { + return; + } + auto widget = (ChatsFiltersTabs::ShiftedSection*)(nullptr); + for (auto i = 0; i != _layout->_sections.size(); ++i) { + auto §ion = _layout->_sections[i]; + if ((position.x() >= section.section->left) + && (position.x() < (section.section->left + section.section->width))) { + widget = §ion; + break; + } + } + cancelCurrent(); + _currentWidget = widget->section; + _currentShiftedWidget = widget; + _currentStart = globalPosition.x(); +} + +void ChatsFiltersTabsReorder::mouseMove(QPoint position) { + if (!_currentWidget) { + // if (_currentWidget != widget) { + return; + } else if (_currentState != State::Started) { + checkForStart(position); + } else { + updateOrder(indexOf(_currentWidget), position); + } +} + +void ChatsFiltersTabsReorder::mouseRelease(Qt::MouseButton button) { + if (button != Qt::LeftButton) { + return; + } + finishReordering(); +} + +void ChatsFiltersTabsReorder::cancelCurrent() { + if (_currentWidget) { + cancelCurrent(indexOf(_currentWidget)); + } +} + +void ChatsFiltersTabsReorder::cancelCurrent(int index) { + Expects(_currentWidget != nullptr); + + if (_currentState == State::Started) { + _currentState = State::Cancelled; + _updates.fire({ _currentWidget, index, index, _currentState }); + } + _currentWidget = nullptr; + _currentShiftedWidget = nullptr; + for (auto i = 0, count = int(_entries.size()); i != count; ++i) { + moveToShift(i, 0); + } +} + +void ChatsFiltersTabsReorder::finishReordering() { + if (_scroll) { + _scrollAnimation.stop(); + } + finishCurrent(); +} + +void ChatsFiltersTabsReorder::finishCurrent() { + if (!_currentWidget) { + return; + } + const auto index = indexOf(_currentWidget); + if (_currentDesiredIndex == index || _currentState != State::Started) { + cancelCurrent(index); + return; + } + const auto result = _currentDesiredIndex; + const auto widget = _currentWidget; + _currentState = State::Cancelled; + _currentWidget = nullptr; + _currentShiftedWidget = nullptr; + + auto ¤t = _entries[index]; + const auto width = current.widget->width; + if (index < result) { + auto sum = 0; + for (auto i = index; i != result; ++i) { + auto &entry = _entries[i + 1]; + const auto widget = entry.widget; + entry.deltaShift += width; + updateShift(widget, i + 1); + sum += widget->width; + } + current.finalShift -= sum; + } else if (index > result) { + auto sum = 0; + for (auto i = result; i != index; ++i) { + auto &entry = _entries[i]; + const auto widget = entry.widget; + entry.deltaShift -= width; + updateShift(widget, i); + sum += widget->width; + } + current.finalShift += sum; + } + if (!(current.finalShift + current.deltaShift)) { + current.shift = 0; + _layout->setHorizontalShift(index, 0); + } + base::reorder(_entries, index, result); + _layout->reorderSections(index, _currentDesiredIndex); + for (auto i = 0; i != _layout->sectionsRef().size(); ++i) { + _entries[i].widget = &_layout->sectionsRef()[i]; + moveToShift(i, 0); + } + + _updates.fire({ widget, index, result, State::Applied }); +} + +void ChatsFiltersTabsReorder::moveToShift(int index, int shift) { + auto &entry = _entries[index]; + if (entry.finalShift + entry.deltaShift == shift) { + return; + } + const auto widget = entry.widget; + entry.shiftAnimation.start( + [=, this] { updateShift(widget, index); }, + entry.finalShift, + shift - entry.deltaShift, + st::slideWrapDuration); + entry.finalShift = shift - entry.deltaShift; +} + +void ChatsFiltersTabsReorder::updateShift( + not_null widget, + int indexHint) { + Expects(indexHint >= 0 && indexHint < _entries.size()); + + const auto index = (_entries[indexHint].widget == widget) + ? indexHint + : indexOf(widget); + auto &entry = _entries[index]; + entry.shift = base::SafeRound( + entry.shiftAnimation.value(entry.finalShift) + ) + entry.deltaShift; + if (entry.deltaShift && !entry.shiftAnimation.animating()) { + entry.finalShift += entry.deltaShift; + entry.deltaShift = 0; + } + _layout->setHorizontalShift(index, entry.shift); +} + +int ChatsFiltersTabsReorder::indexOf(not_null widget) const { + const auto i = ranges::find(_entries, widget, &Entry::widget); + Assert(i != end(_entries)); + return i - begin(_entries); +} + +auto ChatsFiltersTabsReorder::updates() const -> rpl::producer { + return _updates.events(); +} + +void ChatsFiltersTabsReorder::updateScrollCallback() { + if (!_scroll) { + return; + } + const auto delta = deltaFromEdge(); + const auto oldLeft = _scroll->scrollLeft(); + _scroll->horizontalScrollBar()->setValue(oldLeft + delta); + const auto newLeft = _scroll->scrollLeft(); + + _currentStart += oldLeft - newLeft; + if (newLeft == 0 || newLeft == _scroll->scrollLeftMax()) { + _scrollAnimation.stop(); + } +} + +void ChatsFiltersTabsReorder::checkForScrollAnimation() { + if (!_scroll || !deltaFromEdge() || _scrollAnimation.animating()) { + return; + } + _scrollAnimation.start(); +} + +int ChatsFiltersTabsReorder::deltaFromEdge() { + Expects(_currentWidget != nullptr); + Expects(_currentShiftedWidget != nullptr); + Expects(_scroll); + + const auto globalPosition = _layout->mapToGlobal( + QPoint( + _currentWidget->left + _currentShiftedWidget->horizontalShift, + 0)); + const auto localLeft = _scroll->mapFromGlobal(globalPosition).x(); + const auto localRight = localLeft + + _currentWidget->width + - _scroll->width(); + + const auto isLeftEdge = (localLeft < 0); + const auto isRightEdge = (localRight > 0); + if (!isLeftEdge && !isRightEdge) { + _scrollAnimation.stop(); + return 0; + } + return int((isRightEdge ? localRight : localLeft) * kScrollFactor); +} + +} // namespace Ui diff --git a/Telegram/SourceFiles/ui/widgets/chat_filters_tabs_slider_reorder.h b/Telegram/SourceFiles/ui/widgets/chat_filters_tabs_slider_reorder.h new file mode 100644 index 000000000..4a167d366 --- /dev/null +++ b/Telegram/SourceFiles/ui/widgets/chat_filters_tabs_slider_reorder.h @@ -0,0 +1,98 @@ +/* +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 +*/ +#pragma once + +#include "ui/effects/animations.h" +#include "ui/widgets/chat_filters_tabs_slider.h" + +namespace Ui { + +class ScrollArea; + +class ChatsFiltersTabsReorder final { +public: + using Section = ChatsFiltersTabs::Section; + enum class State : uchar { + Started, + Applied, + Cancelled, + }; + + struct Single { + not_null widget; + int oldPosition = 0; + int newPosition = 0; + State state = State::Started; + }; + + ChatsFiltersTabsReorder( + not_null layout, + not_null scroll); + ChatsFiltersTabsReorder(not_null layout); + + void start(); + void cancel(); + void finishReordering(); + void addPinnedInterval(int from, int length); + void clearPinnedIntervals(); + [[nodiscard]] rpl::producer updates() const; + +private: + struct Entry { + not_null widget; + Ui::Animations::Simple shiftAnimation; + int shift = 0; + int finalShift = 0; + int deltaShift = 0; + }; + struct Interval { + [[nodiscard]] bool isIn(int index) const; + + int from = 0; + int length = 0; + }; + + void mousePress(Qt::MouseButton button, QPoint position, QPoint global); + void mouseMove(QPoint position); + void mouseRelease(Qt::MouseButton button); + + void checkForStart(QPoint position); + void updateOrder(int index, QPoint position); + void cancelCurrent(); + void finishCurrent(); + void cancelCurrent(int index); + + [[nodiscard]] int indexOf(not_null widget) const; + void moveToShift(int index, int shift); + void updateShift(not_null widget, int indexHint); + + void updateScrollCallback(); + void checkForScrollAnimation(); + [[nodiscard]] int deltaFromEdge(); + + [[nodiscard]] bool isIndexPinned(int index) const; + + const not_null _layout; + Ui::ScrollArea *_scroll = nullptr; + + Ui::Animations::Basic _scrollAnimation; + + std::vector _pinnedIntervals; + + Section *_currentWidget = nullptr; + ChatsFiltersTabs::ShiftedSection *_currentShiftedWidget = nullptr; + int _currentStart = 0; + int _currentDesiredIndex = 0; + State _currentState = State::Cancelled; + std::vector _entries; + rpl::event_stream _updates; + rpl::lifetime _lifetime; + +}; + +} // namespace Ui diff --git a/Telegram/cmake/td_ui.cmake b/Telegram/cmake/td_ui.cmake index f45d83aff..c7af0a748 100644 --- a/Telegram/cmake/td_ui.cmake +++ b/Telegram/cmake/td_ui.cmake @@ -423,6 +423,8 @@ PRIVATE ui/widgets/chat_filters_tabs_slider.cpp ui/widgets/chat_filters_tabs_slider.h + ui/widgets/chat_filters_tabs_slider_reorder.cpp + ui/widgets/chat_filters_tabs_slider_reorder.h ui/widgets/color_editor.cpp ui/widgets/color_editor.h ui/widgets/continuous_sliders.cpp diff --git a/Telegram/lib_base b/Telegram/lib_base index 21d1ac8bf..2b622fd0b 160000 --- a/Telegram/lib_base +++ b/Telegram/lib_base @@ -1 +1 @@ -Subproject commit 21d1ac8bfcca03f67d7f6df75e265cd5597dc101 +Subproject commit 2b622fd0b223ed6266a32dc07382975769cc031c