/* 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/controls/subsection_tabs_slider_reorder.h" #include "ui/controls/subsection_tabs_slider.h" #include "ui/widgets/scroll_area.h" #include "ui/controls/subsection_tabs_slider.h" #include "styles/style_basic.h" #include namespace Ui { namespace { constexpr auto kScrollFactor = 0.05; } // namespace SubsectionSliderReorder::SubsectionSliderReorder( not_null slider, not_null scroll) : _slider(slider) , _scroll(scroll) , _scrollAnimation([=] { updateScrollCallback(); }) { } SubsectionSliderReorder::SubsectionSliderReorder( not_null slider) : _slider(slider) { } SubsectionSliderReorder::~SubsectionSliderReorder() { cancel(); } void SubsectionSliderReorder::cancel() { if (_currentButton) { cancelCurrent(indexOf(_currentButton)); } _lifetime.destroy(); _entries.clear(); } void SubsectionSliderReorder::start() { const auto count = _slider->sectionsCount(); if (count < 2) { return; } for (auto i = 0; i != count; ++i) { const auto button = _slider->buttonAt(i); const auto eventsProducer = _proxyButtonCallback ? _proxyButtonCallback(i) : button; eventsProducer->events( ) | rpl::on_next_done([=](not_null e) { switch (e->type()) { case QEvent::MouseMove: mouseMove( button, static_cast(e.get())->globalPos()); break; case QEvent::MouseButtonPress: { const auto event = static_cast(e.get()); mousePress(button, event->button(), event->globalPos()); break; } case QEvent::MouseButtonRelease: mouseRelease(static_cast(e.get())->button()); break; } }, [=] { cancel(); }, _lifetime); _entries.push_back({ button }); } } void SubsectionSliderReorder::addPinnedInterval(int from, int length) { _pinnedIntervals.push_back({ from, length }); } void SubsectionSliderReorder::clearPinnedIntervals() { _pinnedIntervals.clear(); } void SubsectionSliderReorder::setMouseEventProxy(ProxyCallback callback) { _proxyButtonCallback = std::move(callback); } bool SubsectionSliderReorder::Interval::isIn(int index) const { return (index >= from) && (index < (from + length)); } bool SubsectionSliderReorder::isIndexPinned(int index) const { return ranges::any_of(_pinnedIntervals, [&](const Interval &i) { return i.isIn(index); }); } void SubsectionSliderReorder::mouseMove( not_null button, QPoint position) { if (_currentButton != button) { return; } else if (_currentState != State::Started) { checkForStart(position); } else { updateOrder(indexOf(_currentButton), position); checkForScrollAnimation(); } } void SubsectionSliderReorder::checkForStart(QPoint position) { const auto shift = _slider->isVertical() ? (position.y() - _currentStart) : (position.x() - _currentStart); const auto delta = QApplication::startDragDistance(); if (std::abs(shift) <= delta) { return; } _currentButton->raise(); _currentState = State::Started; _currentStart += (shift > 0) ? delta : -delta; const auto index = indexOf(_currentButton); _currentDesiredIndex = index; _updates.fire({ _currentButton, index, index, _currentState }); updateOrder(index, position); } void SubsectionSliderReorder::updateOrder(int index, QPoint position) { if (isIndexPinned(index)) { return; } const auto shift = _slider->isVertical() ? (position.y() - _currentStart) : (position.x() - _currentStart); auto ¤t = _entries[index]; current.shiftAnimation.stop(); current.shift = current.finalShift = shift; _slider->setButtonShift(index, shift); const auto count = _entries.size(); const auto currentSize = _slider->isVertical() ? _currentButton->height() : _currentButton->width(); const auto currentMiddle = _slider->isVertical() ? (_currentButton->y() + currentSize / 2) : (_currentButton->x() + currentSize / 2); _currentDesiredIndex = index; if (shift > 0) { auto top = _slider->isVertical() ? (_currentButton->y() - shift) : (_currentButton->x() - shift); for (auto next = index + 1; next != count; ++next) { if (isIndexPinned(next)) { return; } const auto &entry = _entries[next]; top += _slider->isVertical() ? entry.button->height() : entry.button->width(); if (currentMiddle < top) { moveToShift(next, 0); } else { _currentDesiredIndex = next; moveToShift(next, -currentSize); } } 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 &entry = _entries[prev]; const auto entryPos = _slider->isVertical() ? (entry.button->y() - entry.shift) : (entry.button->x() - entry.shift); if (currentMiddle >= entryPos + currentSize) { moveToShift(prev, 0); } else { _currentDesiredIndex = prev; moveToShift(prev, currentSize); } } } } void SubsectionSliderReorder::mousePress( not_null button, Qt::MouseButton btn, QPoint position) { if (btn != Qt::LeftButton) { return; } cancelCurrent(); _currentButton = button; _currentStart = _slider->isVertical() ? position.y() : position.x(); } void SubsectionSliderReorder::mouseRelease(Qt::MouseButton button) { if (button != Qt::LeftButton) { return; } finishReordering(); } void SubsectionSliderReorder::cancelCurrent() { if (_currentButton) { cancelCurrent(indexOf(_currentButton)); } } void SubsectionSliderReorder::cancelCurrent(int index) { Expects(_currentButton != nullptr); if (_currentState == State::Started) { _currentState = State::Cancelled; _updates.fire({ _currentButton, index, index, _currentState }); } _currentButton = nullptr; for (auto i = 0, count = int(_entries.size()); i != count; ++i) { moveToShift(i, 0); } } void SubsectionSliderReorder::finishReordering() { if (_scroll) { _scrollAnimation.stop(); } finishCurrent(); } void SubsectionSliderReorder::finishCurrent() { if (!_currentButton) { return; } const auto index = indexOf(_currentButton); if (_currentDesiredIndex == index || _currentState != State::Started) { cancelCurrent(index); return; } const auto result = _currentDesiredIndex; const auto button = _currentButton; _currentState = State::Cancelled; _currentButton = nullptr; auto ¤t = _entries[index]; const auto size = _slider->isVertical() ? button->height() : button->width(); if (index < result) { auto sum = 0; for (auto i = index; i != result; ++i) { auto &entry = _entries[i + 1]; const auto btn = entry.button; entry.deltaShift += size; updateShift(btn, i + 1); sum += _slider->isVertical() ? btn->height() : btn->width(); } current.finalShift -= sum; } else if (index > result) { auto sum = 0; for (auto i = result; i != index; ++i) { auto &entry = _entries[i]; const auto btn = entry.button; entry.deltaShift -= size; updateShift(btn, i); sum += _slider->isVertical() ? btn->height() : btn->width(); } current.finalShift += sum; } if (!(current.finalShift + current.deltaShift)) { current.shift = 0; _slider->setButtonShift(index, 0); } base::reorder(_entries, index, result); _slider->reorderButtons(index, result); for (auto i = 0, count = int(_entries.size()); i != count; ++i) { moveToShift(i, 0); } _updates.fire({ button, index, result, State::Applied }); } void SubsectionSliderReorder::moveToShift(int index, int shift) { auto &entry = _entries[index]; if (entry.finalShift + entry.deltaShift == shift) { return; } const auto button = entry.button; entry.shiftAnimation.start( [=] { updateShift(button, index); }, entry.finalShift, shift - entry.deltaShift, st::slideWrapDuration); entry.finalShift = shift - entry.deltaShift; } void SubsectionSliderReorder::updateShift( not_null button, int indexHint) { Expects(indexHint >= 0 && indexHint < _entries.size()); const auto index = (_entries[indexHint].button == button) ? indexHint : indexOf(button); 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; } _slider->setButtonShift(index, entry.shift); } auto SubsectionSliderReorder::updates() const -> rpl::producer { return _updates.events(); } int SubsectionSliderReorder::indexOf( not_null button) const { const auto i = ranges::find(_entries, button, &Entry::button); Assert(i != end(_entries)); return i - begin(_entries); } void SubsectionSliderReorder::updateScrollCallback() { if (!_scroll) { return; } const auto delta = deltaFromEdge(); if (_slider->isVertical()) { const auto oldTop = _scroll->scrollTop(); _scroll->scrollToY(oldTop + delta); const auto newTop = _scroll->scrollTop(); _currentStart += oldTop - newTop; if (newTop == 0 || newTop == _scroll->scrollTopMax()) { _scrollAnimation.stop(); } } else { const auto oldLeft = _scroll->scrollLeft(); _scroll->scrollToX(oldLeft + delta); const auto newLeft = _scroll->scrollLeft(); _currentStart += oldLeft - newLeft; if (newLeft == 0 || newLeft == _scroll->scrollLeftMax()) { _scrollAnimation.stop(); } } } void SubsectionSliderReorder::checkForScrollAnimation() { if (!_scroll || !deltaFromEdge() || _scrollAnimation.animating()) { return; } _scrollAnimation.start(); } int SubsectionSliderReorder::deltaFromEdge() { Expects(_currentButton != nullptr); Expects(_scroll); const auto globalPosition = _currentButton->mapToGlobal(QPoint(0, 0)); const auto localPos = _scroll->mapFromGlobal(globalPosition); const auto localTop = _slider->isVertical() ? localPos.y() : localPos.x(); const auto buttonSize = _slider->isVertical() ? _currentButton->height() : _currentButton->width(); const auto scrollSize = _slider->isVertical() ? _scroll->height() : _scroll->width(); const auto localBottom = localTop + buttonSize - scrollSize; const auto isTopEdge = (localTop < 0); const auto isBottomEdge = (localBottom > 0); if (!isTopEdge && !isBottomEdge) { _scrollAnimation.stop(); return 0; } return int((isBottomEdge ? localBottom : localTop) * kScrollFactor); } } // namespace Ui