From 540fa0e66978d81539b82910b37c9e509d380479 Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Mon, 17 Feb 2025 15:13:56 +0300 Subject: [PATCH] Added ability to swipe-to-back to history widget. --- .../history/history_inner_widget.cpp | 37 ++++- .../history/history_inner_widget.h | 3 +- .../history/history_view_swipe.cpp | 133 +++++++++++++++++- .../SourceFiles/history/history_view_swipe.h | 9 +- .../history/history_view_swipe_data.h | 5 + .../SourceFiles/history/history_widget.cpp | 2 + .../view/history_view_replies_section.cpp | 4 +- Telegram/SourceFiles/ui/chat/chat.style | 2 + 8 files changed, 184 insertions(+), 11 deletions(-) diff --git a/Telegram/SourceFiles/history/history_inner_widget.cpp b/Telegram/SourceFiles/history/history_inner_widget.cpp index e470c651ee..6dcf978acd 100644 --- a/Telegram/SourceFiles/history/history_inner_widget.cpp +++ b/Telegram/SourceFiles/history/history_inner_widget.cpp @@ -440,7 +440,7 @@ HistoryInner::HistoryInner( }, _scroll->lifetime()); setupSharingDisallowed(); - setupSwipeReply(); + setupSwipeReplyAndBack(); } void HistoryInner::reactionChosen(const ChosenReaction &reaction) { @@ -523,12 +523,30 @@ void HistoryInner::setupSharingDisallowed() { }, lifetime()); } -void HistoryInner::setupSwipeReply() { - if (_peer && _peer->isChannel() && !_peer->isMegagroup()) { +void HistoryInner::setupSwipeReplyAndBack() { + if (!_peer) { return; } + const auto peer = _peer; HistoryView::SetupSwipeHandler(this, _scroll, [=, history = _history]( HistoryView::ChatPaintGestureHorizontalData data) { + if (data.translation > 0) { + if (!_swipeBackData.callback) { + _swipeBackData = HistoryView::SetupSwipeBack( + _widget, + [=]() -> std::pair { + auto context = preparePaintContext({}); + return { + context.st->msgServiceBg()->c, + context.st->msgServiceFg()->c, + }; + }); + } + _swipeBackData.callback(data); + return; + } else if (_swipeBackData.lifetime) { + _swipeBackData = {}; + } const auto changed = (_gestureHorizontal.msgBareId != data.msgBareId) || (_gestureHorizontal.translation != data.translation) || (_gestureHorizontal.reachRatio != data.reachRatio); @@ -541,9 +559,18 @@ void HistoryInner::setupSwipeReply() { repaintItem(item); } } - }, [=, show = _controller->uiShow()](int cursorTop) { + }, [=, show = _controller->uiShow()]( + int cursorTop, + Qt::LayoutDirection direction) { + if (direction == Qt::RightToLeft) { + return HistoryView::SwipeHandlerFinishData{ + .callback = [=] { _controller->showBackFromStack(); }, + .msgBareId = HistoryView::kMsgBareIdSwipeBack, + }; + } auto result = HistoryView::SwipeHandlerFinishData(); - if (inSelectionMode().inSelectionMode) { + if (inSelectionMode().inSelectionMode + || (peer->isChannel() && !peer->isMegagroup())) { return result; } enumerateItems([&]( diff --git a/Telegram/SourceFiles/history/history_inner_widget.h b/Telegram/SourceFiles/history/history_inner_widget.h index 1288f6e7b4..855add11bc 100644 --- a/Telegram/SourceFiles/history/history_inner_widget.h +++ b/Telegram/SourceFiles/history/history_inner_widget.h @@ -428,7 +428,7 @@ private: void reactionChosen(const ChosenReaction &reaction); void setupSharingDisallowed(); - void setupSwipeReply(); + void setupSwipeReplyAndBack(); [[nodiscard]] bool hasCopyRestriction(HistoryItem *item = nullptr) const; [[nodiscard]] bool hasCopyMediaRestriction( not_null item) const; @@ -544,6 +544,7 @@ private: base::Timer _touchScrollTimer; HistoryView::ChatPaintGestureHorizontalData _gestureHorizontal; + HistoryView::SwipeBackResult _swipeBackData; // _menu must be destroyed before _whoReactedMenuLifetime. rpl::lifetime _whoReactedMenuLifetime; diff --git a/Telegram/SourceFiles/history/history_view_swipe.cpp b/Telegram/SourceFiles/history/history_view_swipe.cpp index 16190bc261..7d6c54ca27 100644 --- a/Telegram/SourceFiles/history/history_view_swipe.cpp +++ b/Telegram/SourceFiles/history/history_view_swipe.cpp @@ -13,9 +13,12 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "base/event_filter.h" #include "history/history_view_swipe_data.h" #include "ui/chat/chat_style.h" +#include "ui/painter.h" +#include "ui/rect.h" #include "ui/ui_utility.h" #include "ui/widgets/elastic_scroll.h" #include "ui/widgets/scroll_area.h" +#include "styles/style_chat.h" #include @@ -30,7 +33,7 @@ void SetupSwipeHandler( not_null widget, not_null scroll, Fn update, - Fn generateFinishByTop, + Fn generateFinish, rpl::producer dontStart) { constexpr auto kThresholdWidth = 50; constexpr auto kMaxRatio = 1.5; @@ -145,8 +148,9 @@ void SetupSwipeHandler( state->startAt = args.position; state->delta = QPointF(); state->cursorTop = widget->mapFromGlobal(args.globalCursor).y(); - state->finishByTopData = generateFinishByTop( - state->cursorTop); + state->finishByTopData = generateFinish( + state->cursorTop, + state->direction.value_or(Qt::RightToLeft)); if (!state->finishByTopData.callback) { setOrientation(Qt::Vertical); } @@ -279,4 +283,127 @@ void SetupSwipeHandler( base::install_event_filter(widget, filter)); } +SwipeBackResult SetupSwipeBack( + not_null widget, + Fn()> colors) { + struct State { + base::unique_qptr back; + ChatPaintGestureHorizontalData data; + }; + + constexpr auto kMaxRightOffset = 0.5; + constexpr auto kMaxLeftOffset = 0.8; + constexpr auto kIdealSize = 100; + const auto maxOffset = st::swipeBackSize * kMaxRightOffset; + const auto sizeRatio = st::swipeBackSize + / style::ConvertFloatScale(kIdealSize); + + auto lifetime = rpl::lifetime(); + const auto state = lifetime.make_state(); + + const auto paintCallback = [=] { + const auto [bg, fg] = colors(); + const auto arrowPen = QPen( + fg, + st::lineWidth * 3 * sizeRatio, + Qt::SolidLine, + Qt::RoundCap); + return [=] { + auto p = QPainter(state->back); + + constexpr auto kBouncePart = 0.25; + constexpr auto kStrokeWidth = 2.; + constexpr auto kWaveWidth = 10.; + const auto ratio = std::min(state->data.ratio, 1.); + const auto reachRatio = state->data.reachRatio; + const auto rect = state->back->rect() + - Margins(state->back->width() / 4); + const auto center = rect::center(rect); + const auto strokeWidth = style::ConvertFloatScale(kStrokeWidth) + * sizeRatio; + + const auto reachScale = std::clamp( + (reachRatio > kBouncePart) + ? (kBouncePart * 2 - reachRatio) + : reachRatio, + 0., + 1.); + auto pen = QPen(bg); + pen.setWidthF(strokeWidth - (1. * (reachScale / kBouncePart))); + const auto arcRect = rect - Margins(strokeWidth); + auto hq = PainterHighQualityEnabler(p); + p.setOpacity(ratio); + if (reachScale) { + const auto scale = (1. + 1. * reachScale); + p.translate(center); + p.scale(scale, scale); + p.translate(-center); + } + { + p.setPen(Qt::NoPen); + p.setBrush(bg); + p.drawEllipse(rect); + p.drawEllipse(rect); + p.setPen(arrowPen); + p.setBrush(Qt::NoBrush); + const auto halfSize = rect.width() / 2; + const auto arrowSize = halfSize / 2; + const auto arrowHalf = arrowSize / 2; + const auto arrowX = st::swipeBackSize / 8 + + rect.x() + + halfSize + - arrowHalf; + const auto arrowY = rect.y() + halfSize; + + auto arrowPath = QPainterPath(); + arrowPath.moveTo(arrowX + arrowSize, arrowY); + arrowPath.lineTo(arrowX, arrowY); + arrowPath.lineTo(arrowX + arrowHalf, arrowY - arrowHalf); + arrowPath.moveTo(arrowX, arrowY); + arrowPath.lineTo(arrowX + arrowHalf, arrowY + arrowHalf); + + p.drawPath(arrowPath); + } + if (reachRatio) { + p.setPen(pen); + p.setBrush(Qt::NoBrush); + const auto w = style::ConvertFloatScale(kWaveWidth) + * sizeRatio; + p.setOpacity(ratio - reachRatio); + p.drawArc( + arcRect + Margins(reachRatio * reachRatio * w), + arc::kQuarterLength, + arc::kFullLength); + } + }; + }; + + const auto callback = ([=](ChatPaintGestureHorizontalData data) { + const auto ratio = std::min(1.0, data.ratio); + state->data = std::move(data); + if (ratio > 0) { + if (!state->back) { + state->back = base::make_unique_q(widget); + const auto raw = state->back.get(); + raw->paintRequest( + ) | rpl::start_with_next(paintCallback(), raw->lifetime()); + raw->setAttribute(Qt::WA_TransparentForMouseEvents); + raw->resize(Size(st::swipeBackSize)); + raw->show(); + raw->raise(); + } + state->back->moveToLeft( + anim::interpolate( + -st::swipeBackSize * kMaxLeftOffset, + maxOffset - st::swipeBackSize, + ratio), + (widget->height() - state->back->height()) / 2); + state->back->update(); + } else if (state->back) { + state->back = nullptr; + } + }); + return { std::move(lifetime), std::move(callback) }; +} + } // namespace HistoryView diff --git a/Telegram/SourceFiles/history/history_view_swipe.h b/Telegram/SourceFiles/history/history_view_swipe.h index fe9ec2ea02..300e390402 100644 --- a/Telegram/SourceFiles/history/history_view_swipe.h +++ b/Telegram/SourceFiles/history/history_view_swipe.h @@ -15,6 +15,9 @@ class ScrollArea; namespace HistoryView { struct ChatPaintGestureHorizontalData; +struct SwipeBackResult; + +constexpr auto kMsgBareIdSwipeBack = std::numeric_limits::max() - 77; struct SwipeHandlerFinishData { Fn callback; @@ -25,7 +28,11 @@ void SetupSwipeHandler( not_null widget, not_null scroll, Fn update, - Fn generateFinishByTop, + Fn generateFinishByTop, rpl::producer dontStart = nullptr); +SwipeBackResult SetupSwipeBack( + not_null widget, + Fn()> colors); + } // namespace HistoryView diff --git a/Telegram/SourceFiles/history/history_view_swipe_data.h b/Telegram/SourceFiles/history/history_view_swipe_data.h index c3dd9eb776..3d2d356c4c 100644 --- a/Telegram/SourceFiles/history/history_view_swipe_data.h +++ b/Telegram/SourceFiles/history/history_view_swipe_data.h @@ -17,4 +17,9 @@ struct ChatPaintGestureHorizontalData { int cursorTop = 0; }; +struct SwipeBackResult final { + rpl::lifetime lifetime; + Fn callback; +}; + } // namespace HistoryView diff --git a/Telegram/SourceFiles/history/history_widget.cpp b/Telegram/SourceFiles/history/history_widget.cpp index 48cecb1301..5c9f70dbf1 100644 --- a/Telegram/SourceFiles/history/history_widget.cpp +++ b/Telegram/SourceFiles/history/history_widget.cpp @@ -58,6 +58,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "base/qt_signal_producer.h" #include "base/qt/qt_key_modifiers.h" #include "base/unixtime.h" +#include "history/history_view_swipe.h" +#include "history/history_view_swipe_data.h" #include "base/call_delayed.h" #include "data/business/data_shortcut_messages.h" #include "data/components/credits.h" diff --git a/Telegram/SourceFiles/history/view/history_view_replies_section.cpp b/Telegram/SourceFiles/history/view/history_view_replies_section.cpp index dfd94af4fb..bc8219ff5b 100644 --- a/Telegram/SourceFiles/history/view/history_view_replies_section.cpp +++ b/Telegram/SourceFiles/history/view/history_view_replies_section.cpp @@ -904,7 +904,9 @@ void RepliesWidget::setupSwipeReply() { _history->owner().requestItemRepaint(item); } } - }, [=, show = controller()->uiShow()](int cursorTop) { + }, [=, show = controller()->uiShow()]( + int cursorTop, + Qt::LayoutDirection direction) { auto result = HistoryView::SwipeHandlerFinishData(); if (_inner->elementInSelectionMode(nullptr).inSelectionMode) { return result; diff --git a/Telegram/SourceFiles/ui/chat/chat.style b/Telegram/SourceFiles/ui/chat/chat.style index 1b0c786e90..3a44fffc2b 100644 --- a/Telegram/SourceFiles/ui/chat/chat.style +++ b/Telegram/SourceFiles/ui/chat/chat.style @@ -1245,3 +1245,5 @@ newPeerUserpics: GroupCallUserpics { } newPeerUserpicsPadding: margins(0px, 3px, 0px, 0px); newPeerWidth: 320px; + +swipeBackSize: 150px;