Added initial implementation of class for reorder of tabs slider.

This commit is contained in:
23rd 2024-11-04 20:32:24 +03:00
parent 63a8fe7ee8
commit 3e413a036f
4 changed files with 474 additions and 1 deletions

View file

@ -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 <QScrollBar>
#include <QtGui/QtEvents>
#include <QtWidgets/QApplication>
namespace Ui {
namespace {
constexpr auto kScrollFactor = 0.05;
} // namespace
ChatsFiltersTabsReorder::ChatsFiltersTabsReorder(
not_null<ChatsFiltersTabs*> layout,
not_null<ScrollArea*> scroll)
: _layout(layout)
, _scroll(scroll)
, _scrollAnimation([this] { updateScrollCallback(); }) {
}
ChatsFiltersTabsReorder::ChatsFiltersTabsReorder(
not_null<ChatsFiltersTabs*> 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<QEvent*> e) {
switch (e->type()) {
case QEvent::MouseMove:
mouseMove(static_cast<QMouseEvent*>(e.get())->globalPos());
break;
case QEvent::MouseButtonPress: {
const auto m = static_cast<QMouseEvent*>(e.get());
mousePress(m->button(), m->pos(), m->globalPos());
break;
}
case QEvent::MouseButtonRelease:
mouseRelease(static_cast<QMouseEvent*>(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 &current = _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 &section = _layout->_sections[i];
if ((position.x() >= section.section->left)
&& (position.x() < (section.section->left + section.section->width))) {
widget = &section;
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 &current = _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<Section*> 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<Section*> 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<Single> {
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

View file

@ -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<Section*> widget;
int oldPosition = 0;
int newPosition = 0;
State state = State::Started;
};
ChatsFiltersTabsReorder(
not_null<ChatsFiltersTabs*> layout,
not_null<ScrollArea*> scroll);
ChatsFiltersTabsReorder(not_null<ChatsFiltersTabs*> layout);
void start();
void cancel();
void finishReordering();
void addPinnedInterval(int from, int length);
void clearPinnedIntervals();
[[nodiscard]] rpl::producer<Single> updates() const;
private:
struct Entry {
not_null<Section*> 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<Section*> widget) const;
void moveToShift(int index, int shift);
void updateShift(not_null<Section*> widget, int indexHint);
void updateScrollCallback();
void checkForScrollAnimation();
[[nodiscard]] int deltaFromEdge();
[[nodiscard]] bool isIndexPinned(int index) const;
const not_null<ChatsFiltersTabs*> _layout;
Ui::ScrollArea *_scroll = nullptr;
Ui::Animations::Basic _scrollAnimation;
std::vector<Interval> _pinnedIntervals;
Section *_currentWidget = nullptr;
ChatsFiltersTabs::ShiftedSection *_currentShiftedWidget = nullptr;
int _currentStart = 0;
int _currentDesiredIndex = 0;
State _currentState = State::Cancelled;
std::vector<Entry> _entries;
rpl::event_stream<Single> _updates;
rpl::lifetime _lifetime;
};
} // namespace Ui

View file

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

@ -1 +1 @@
Subproject commit 21d1ac8bfcca03f67d7f6df75e265cd5597dc101
Subproject commit 2b622fd0b223ed6266a32dc07382975769cc031c