diff --git a/Telegram/SourceFiles/info/peer_gifts/info_peer_gifts_common.cpp b/Telegram/SourceFiles/info/peer_gifts/info_peer_gifts_common.cpp index fb75770aa3..be341c76e2 100644 --- a/Telegram/SourceFiles/info/peer_gifts/info_peer_gifts_common.cpp +++ b/Telegram/SourceFiles/info/peer_gifts/info_peer_gifts_common.cpp @@ -537,6 +537,21 @@ void GiftButton::contextMenuEvent(QContextMenuEvent *e) { : QCursor::pos()); } +void GiftButton::mousePressEvent(QMouseEvent *e) { + _mouseEvents.fire_copy(e); + AbstractButton::mousePressEvent(e); +} + +void GiftButton::mouseMoveEvent(QMouseEvent *e) { + _mouseEvents.fire_copy(e); + AbstractButton::mouseMoveEvent(e); +} + +void GiftButton::mouseReleaseEvent(QMouseEvent *e) { + _mouseEvents.fire_copy(e); + AbstractButton::mouseReleaseEvent(e); +} + void GiftButton::cacheUniqueBackground( not_null unique, int width, @@ -982,7 +997,7 @@ QSize Delegate::buttonSize() { return _single; } -QMargins Delegate::buttonExtend() { +QMargins Delegate::buttonExtend() const { return st::defaultDropdownMenu.wrap.shadow.extend; } diff --git a/Telegram/SourceFiles/info/peer_gifts/info_peer_gifts_common.h b/Telegram/SourceFiles/info/peer_gifts/info_peer_gifts_common.h index fffb28bd47..73ebae958d 100644 --- a/Telegram/SourceFiles/info/peer_gifts/info_peer_gifts_common.h +++ b/Telegram/SourceFiles/info/peer_gifts/info_peer_gifts_common.h @@ -147,7 +147,7 @@ public: [[nodiscard]] virtual TextWithEntities ministar() = 0; [[nodiscard]] virtual Ui::Text::MarkedContext textContext() = 0; [[nodiscard]] virtual QSize buttonSize() = 0; - [[nodiscard]] virtual QMargins buttonExtend() = 0; + [[nodiscard]] virtual QMargins buttonExtend() const = 0; [[nodiscard]] virtual auto buttonPatternEmoji( not_null unique, Fn repaint) @@ -178,10 +178,17 @@ public: return _contextMenuRequests.events(); } + [[nodiscard]] rpl::producer mouseEvents() const { + return _mouseEvents.events(); + } + private: void paintEvent(QPaintEvent *e) override; void resizeEvent(QResizeEvent *e) override; void contextMenuEvent(QContextMenuEvent *e) override; + void mousePressEvent(QMouseEvent *e) override; + void mouseMoveEvent(QMouseEvent *e) override; + void mouseReleaseEvent(QMouseEvent *e) override; void paintBackground(QPainter &p, const QImage &background); void cacheUniqueBackground( @@ -199,6 +206,7 @@ private: const not_null _delegate; rpl::event_stream _contextMenuRequests; + rpl::event_stream _mouseEvents; QImage _hiddenBgCache; GiftDescriptor _descriptor; Ui::Text::String _text; @@ -247,7 +255,7 @@ public: TextWithEntities ministar() override; Ui::Text::MarkedContext textContext() override; QSize buttonSize() override; - QMargins buttonExtend() override; + QMargins buttonExtend() const override; auto buttonPatternEmoji( not_null unique, Fn repaint) diff --git a/Telegram/SourceFiles/info/peer_gifts/info_peer_gifts_widget.cpp b/Telegram/SourceFiles/info/peer_gifts/info_peer_gifts_widget.cpp index 364bba9732..1be9f54621 100644 --- a/Telegram/SourceFiles/info/peer_gifts/info_peer_gifts_widget.cpp +++ b/Telegram/SourceFiles/info/peer_gifts/info_peer_gifts_widget.cpp @@ -48,6 +48,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "styles/style_menu_icons.h" #include "styles/style_credits.h" // giftBoxPadding +#include + namespace Info::PeerGifts { namespace { @@ -160,6 +162,18 @@ private: uint64 giftId = 0; int index = 0; }; + struct DragState { + bool enabled = false; + int index = -1; + int lastSelected = -1; + QPoint point; + QPoint startPos; + }; + struct ShiftAnimation { + Ui::Animations::Simple xAnimation; + Ui::Animations::Simple yAnimation; + int shift = 0; + }; public: InnerWidget( @@ -175,6 +189,9 @@ private: int visibleTop, int visibleBottom) override; void paintEvent(QPaintEvent *e) override; + void mousePressEvent(QMouseEvent *e) override; + void mouseMoveEvent(QMouseEvent *e) override; + void mouseReleaseEvent(QMouseEvent *e) override; void subscribeToUpdates(); void applyUpdateTo(Entries &entries, const Data::GiftUpdate &update); @@ -190,6 +207,12 @@ private: void refreshAbout(); void refreshCollectionsTabs(); + void updateSelected(); + int giftFromGlobalPos(const QPoint &p) const; + [[nodiscard]] QPoint posFromIndex(int index) const; + [[nodiscard]] bool isDraggedAnimating() const; + void requestReorder(int fromIndex, int toIndex); + void collectionRenamed(int id, QString name); void collectionRemoved(int id); @@ -243,6 +266,10 @@ private: int _visibleFrom = 0; int _visibleTill = 0; + DragState _dragging; + base::flat_map _shiftAnimations; + int _selected = -1; + base::unique_qptr _menu; }; @@ -745,14 +772,33 @@ void InnerWidget::validateButtons() { ) | rpl::start_with_next([=](QPoint point) { showMenuFor(raw, point); }, raw->lifetime()); + + raw->mouseEvents( + ) | rpl::start_with_next([=](QMouseEvent *e) { + switch (e->type()) { + case QEvent::MouseButtonPress: + raw->raise(); + mousePressEvent(e); + break; + case QEvent::MouseMove: + mouseMoveEvent(e); + break; + case QEvent::MouseButtonRelease: + mouseReleaseEvent(e); + break; + default: + break; + } + }, raw->lifetime()); + raw->show(); views.push_back({ .button = std::move(button) }); } } auto &view = views.back(); - const auto callback = [=] { - showGift(index); - }; + const auto callback = _addingToCollectionId + ? Fn([=] { showGift(index); }) + : nullptr; view.index = index; view.manageId = manageId; view.giftId = giftId; @@ -763,7 +809,9 @@ void InnerWidget::validateButtons() { anim::type::instant); } view.button->setDescriptor(descriptor, _mode); - view.button->setClickedCallback(callback); + if (callback) { + view.button->setClickedCallback(callback); + } return true; }; for (auto j = fromRow; j != tillRow; ++j) { @@ -771,8 +819,36 @@ void InnerWidget::validateButtons() { if (!add(i, j)) { break; } - views.back().button->setGeometry( - QRect(QPoint(x, y), _single), + const auto &view = views.back(); + const auto viewIndex = view.index; + auto pos = QPoint(x, y); + + if (_dragging.enabled && viewIndex >= 0) { + if (viewIndex == _dragging.index && !isDraggedAnimating()) { + pos = mapFromGlobal(QCursor::pos()) - _dragging.point; + } else if (viewIndex == _dragging.index && isDraggedAnimating()) { + const auto it = _shiftAnimations.find(viewIndex); + if (it != _shiftAnimations.end()) { + pos = QPoint( + it->second.xAnimation.value(pos.x()), + it->second.yAnimation.value(pos.y())); + } + } else { + const auto it = _shiftAnimations.find(viewIndex); + if (it != _shiftAnimations.end()) { + const auto &entry = it->second; + const auto toPos = posFromIndex(viewIndex + entry.shift); + pos = QPoint( + entry.xAnimation.value(toPos.x()), + entry.yAnimation.value(toPos.y())); + } + } + } else if (_dragging.enabled && viewIndex == _dragging.index) { + pos = mapFromGlobal(QCursor::pos()) - _dragging.point; + } + + view.button->setGeometry( + QRect(pos, _single), _delegate.buttonExtend()); x += _single.width() + skipw; } @@ -1564,6 +1640,339 @@ void InnerWidget::flushCollectionReorder() { _pendingCollectionReorder = false; } +void InnerWidget::mousePressEvent(QMouseEvent *e) { + if (e->button() != Qt::LeftButton || _addingToCollectionId) { + return; + } + const auto index = giftFromGlobalPos(e->globalPos()); + if (index < 0 || index >= _list->size()) { + return; + } + if (_peer->canManageGifts() + && _descriptor.current().collectionId + && _list->size() > 1) { + if (isDraggedAnimating()) { + return; + } + _dragging.enabled = false; + _dragging.index = index; + _dragging.point = mapFromGlobal(e->globalPos()) - posFromIndex(index); + _dragging.startPos = e->globalPos(); + return; + } +} + +void InnerWidget::mouseMoveEvent(QMouseEvent *e) { + updateSelected(); + const auto draggedAnimating = isDraggedAnimating(); + + if (!_dragging.enabled && _dragging.index >= 0) { + const auto distance + = (e->globalPos() - _dragging.startPos).manhattanLength(); + if (distance > QApplication::startDragDistance()) { + _dragging.enabled = true; + } + } + + if (_selected >= 0 && !draggedAnimating) { + _dragging.lastSelected = _selected; + } + + if (_dragging.enabled + && _dragging.index >= 0 + && _dragging.index < _list->size() + && _dragging.lastSelected >= 0 + && !draggedAnimating) { + for (auto i = 0; i < _list->size(); i++) { + if (i == _dragging.index) { + continue; + } + auto &entry = _shiftAnimations[i]; + const auto wasShift = entry.shift; + if ((i >= _dragging.index) && (i <= _dragging.lastSelected)) { + if (entry.shift == 0) { + entry.shift = -1; + } else if (entry.shift == 1) { + entry.shift = 0; + } + } else if ((i < _dragging.index) + && (i >= _dragging.lastSelected)) { + if (entry.shift == 0) { + entry.shift = 1; + } else if (entry.shift == -1) { + entry.shift = 0; + } + } + if ((i < std::min(_dragging.index, _dragging.lastSelected)) + || (i > std::max(_dragging.index, _dragging.lastSelected))) { + entry.shift = 0; + } + if (wasShift != entry.shift) { + const auto fromPoint = posFromIndex(i + wasShift); + const auto toPoint = posFromIndex(i + entry.shift); + const auto toX = float64(toPoint.x()); + const auto toY = float64(toPoint.y()); + const auto ratio = [&] { + const auto fromX = entry.xAnimation.value(toX); + const auto ratioX = std::min(toX, fromX) + / std::max(toX, fromX); + const auto fromY = entry.yAnimation.value(toY); + const auto ratioY = std::min(toY, fromY) + / std::max(toY, fromY); + return (ratioX == 1.) + ? ratioY + : (ratioY == 1.) + ? ratioX + : std::max(ratioX, ratioY); + }(); + if (!entry.xAnimation.animating()) { + entry.xAnimation.stop(); + entry.xAnimation.start( + [this, i](float64 value) { + for (auto &view : _views) { + if (view.index == i && view.button) { + view.button->moveToLeft( + value, + view.button->y()); + } + } + }, + fromPoint.x(), + toX, + st::fadeWrapDuration); + } else { + entry.xAnimation.change( + toX, + st::fadeWrapDuration * (1. - ratio), + anim::linear); + } + if (!entry.yAnimation.animating()) { + entry.yAnimation.stop(); + entry.yAnimation.start( + [this, i](float64 value) { + for (auto &view : _views) { + if (view.index == i && view.button) { + view.button->moveToLeft( + view.button->x(), + value); + } + } + }, + fromPoint.y(), + toY, + st::fadeWrapDuration); + } else { + entry.yAnimation.change( + toY, + st::fadeWrapDuration * (1. - ratio), + anim::linear); + } + } + } + + for (auto &view : _views) { + if (view.index >= 0 && view.button) { + auto pos = posFromIndex(view.index); + + if (view.index == _dragging.index && !isDraggedAnimating()) { + pos = mapFromGlobal(QCursor::pos()) - _dragging.point; + } else if (view.index == _dragging.index + && isDraggedAnimating()) { + const auto it = _shiftAnimations.find(view.index); + if (it != _shiftAnimations.end()) { + pos = QPoint( + it->second.xAnimation.value(pos.x()), + it->second.yAnimation.value(pos.y())); + } + } else { + const auto it = _shiftAnimations.find(view.index); + if (it != _shiftAnimations.end()) { + const auto &entry = it->second; + const auto toPos + = posFromIndex(view.index + entry.shift); + pos = QPoint( + entry.xAnimation.value(toPos.x()), + entry.yAnimation.value(toPos.y())); + } + } + view.button->moveToLeft(pos.x(), pos.y()); + } + } + + update(); + } +} + +void InnerWidget::mouseReleaseEvent(QMouseEvent *e) { + if (_dragging.enabled && _dragging.index >= 0 && !isDraggedAnimating()) { + for (auto &[index, entry] : _shiftAnimations) { + if (index != _dragging.index) { + entry.xAnimation.stop(); + entry.yAnimation.stop(); + if (auto view = ranges::find(_views, index, &View::index); + view != end(_views) && view->button) { + const auto finalPos = posFromIndex(index + entry.shift); + view->button->moveToLeft(finalPos.x(), finalPos.y()); + } + } + } + + const auto fromPos = mapFromGlobal(e->globalPos()) - _dragging.point; + const auto toPos = posFromIndex(_dragging.lastSelected); + const auto wasPosition = _dragging.index; + const auto nowPosition = _dragging.lastSelected; + const auto finish = [=, this] { + base::reorder(*_list, wasPosition, nowPosition); + for (auto &view : _views) { + view.index = base::reorder_index( + view.index, + wasPosition, + nowPosition); + } + requestReorder(wasPosition, nowPosition); + _dragging = {}; + _shiftAnimations.clear(); + }; + auto &entry = _shiftAnimations[_dragging.index]; + entry.xAnimation.stop(); + entry.yAnimation.stop(); + entry.xAnimation.start( + [finish, toPos, this](float64 value) { + const auto index = _dragging.index; + if (value >= toPos.x() + && index >= 0 + && !_shiftAnimations[index].yAnimation.animating()) { + finish(); + } + for (auto &view : _views) { + if (view.index == index && view.button) { + view.button->moveToLeft(value, view.button->y()); + } + } + }, + fromPos.x(), + toPos.x(), + st::fadeWrapDuration); + entry.yAnimation.start( + [finish, toPos, this](float64 value) { + const auto index = _dragging.index; + if (value >= toPos.y() + && index >= 0 + && !_shiftAnimations[index].xAnimation.animating()) { + finish(); + } + for (auto &view : _views) { + if (view.index == index && view.button) { + view.button->moveToLeft(view.button->x(), value); + } + } + }, + fromPos.y(), + toPos.y(), + st::fadeWrapDuration); + } else { + _dragging = {}; + _shiftAnimations.clear(); + const auto index = giftFromGlobalPos(e->globalPos()); + if (index >= 0 && index < _list->size()) { + showGift(index); + } + } +} + +void InnerWidget::updateSelected() { + const auto selected = giftFromGlobalPos(QCursor::pos()); + if (_selected != selected) { + _selected = selected; + } +} + +int InnerWidget::giftFromGlobalPos(const QPoint &p) const { + const auto l = mapFromGlobal(p); + if (!_perRow) { + return -1; + } + const auto padding = st::giftBoxPadding; + const auto vskip = (_collectionsTabs && !_collectionsTabs->isHidden()) + ? (padding.top() + _collectionsTabs->height() + padding.top()) + : padding.bottom(); + const auto row = (l.y() >= vskip) + ? ((l.y() - vskip) / (_single.height() + st::giftBoxGiftSkip.y())) + : -1; + const auto available = width() - padding.left() - padding.right(); + const auto skipw = st::giftBoxGiftSkip.x(); + const auto fullw = _perRow * (_single.width() + skipw) - skipw; + const auto left = padding.left() + (available - fullw) / 2; + const auto col = (l.x() >= left) + ? ((l.x() - left) / (_single.width() + skipw)) + : -1; + if (row >= 0 && col >= 0 && col < _perRow) { + const auto result = row * _perRow + col; + return (result < _list->size()) ? result : -1; + } + return -1; +} + +QPoint InnerWidget::posFromIndex(int index) const { + if (!_perRow) { + return {}; + } + const auto padding = st::giftBoxPadding; + const auto vskip = (_collectionsTabs && !_collectionsTabs->isHidden()) + ? (padding.top() + _collectionsTabs->height() + padding.top()) + : padding.bottom(); + const auto available = width() - padding.left() - padding.right(); + const auto skipw = st::giftBoxGiftSkip.x(); + const auto skiph = st::giftBoxGiftSkip.y(); + const auto fullw = _perRow * (_single.width() + skipw) - skipw; + const auto left = padding.left() + (available - fullw) / 2; + const auto extend = _delegate.buttonExtend(); + return { + left + (index % _perRow) * (_single.width() + skipw) - extend.left(), + vskip + (index / _perRow) * (_single.height() + skiph) - extend.top(), + }; +} + +bool InnerWidget::isDraggedAnimating() const { + if (_dragging.index < 0) { + return false; + } + const auto it = _shiftAnimations.find(_dragging.index); + return (it == _shiftAnimations.end()) + ? false + : (it->second.xAnimation.animating() + || it->second.yAnimation.animating()); +} + +void InnerWidget::requestReorder(int fromIndex, int toIndex) { + if (fromIndex == toIndex || !_peer->canManageGifts()) { + return; + } + const auto collectionId = _descriptor.current().collectionId; + if (!collectionId) { + return; + } + + auto order = QVector(); + order.reserve(_list->size()); + for (const auto &entry : *_list) { + order.push_back(Api::InputSavedStarGiftId(entry.gift.manageId)); + } + + _api.request( + MTPpayments_UpdateStarGiftCollection( + MTP_flags(MTPpayments_UpdateStarGiftCollection::Flag::f_order), + _peer->input, + MTP_int(collectionId), + MTPstring(), + MTPVector(), + MTPVector(), + MTP_vector(order)) + ).fail([show = _window->uiShow()](const MTP::Error &error) { + show->showToast(error.type()); + }).send(); +} + void InnerWidget::reorderCollections( const Ui::SubTabs::ReorderUpdate &update) { reorderCollectionsLocally(update);