Follow the scroll direction in swipe gestures.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Ilya Fedin
2026-08-27 20:14:32 +04:00
committed by John Preston
parent d99b56b5e0
commit b65cd9496a
8 changed files with 95 additions and 42 deletions

View File

@@ -811,19 +811,27 @@ Widget::Widget(
}
void Widget::setupSwipeBack() {
const auto isMainList = [=] {
// The main menu is dragged out from the left side of the window, so it
// always waits past the end of the chats filters, at whichever of them
// the swipe towards it scrolls to nothing - the first one with the
// natural scrolling and the last one without it. Standing anywhere else
// in the filters that swipe still has a filter to move to.
const auto noNearChatsFilter = [=](bool isNext) {
const auto current = controller()->activeChatsFilterCurrent();
const auto &chatsFilters = session().data().chatsFilters();
if (chatsFilters.has()) {
return chatsFilters.defaultId() == current;
if (!chatsFilters.has()) {
return !current;
}
return !current;
return !Window::CheckAndJumpToNearChatsFilter(
controller(),
isNext,
false);
};
auto update = [=](Ui::Controls::SwipeContextData data) {
data.cursorTop -= _inner->y();
if (data.translation != 0) {
if (data.translation < 0
if (data.visualTranslation() < 0
&& _inner
&& (Core::App().settings().quickDialogAction()
!= Ui::QuickDialogAction::Disabled)) {
@@ -862,7 +870,8 @@ void Widget::setupSwipeBack() {
if (_childListShown.current()) {
return Ui::Controls::SwipeHandlerFinishData();
}
const auto isRightToLeft = data.direction == Qt::RightToLeft;
const auto isRightToLeft = data.fingerDirection() == Qt::RightToLeft;
const auto scrollRightToLeft = data.direction == Qt::RightToLeft;
const auto action = Core::App().settings().quickDialogAction();
const auto isDisabled = action == Ui::QuickDialogAction::Disabled;
if (_inner) {
@@ -936,27 +945,33 @@ void Widget::setupSwipeBack() {
}
});
}
if (isRightToLeft && isMainList()) {
_swipeBackIconMirrored = true;
return Ui::Controls::DefaultSwipeBackHandlerFinishData([=] {
_swipeBackIconMirrored = false;
_swipeBackData = {};
if (isMainList()) {
showMainMenu();
}
});
}
const auto next = !scrollRightToLeft;
if (session().data().chatsFilters().has() && isDisabled) {
_swipeBackMirrored = !isRightToLeft;
using namespace Window;
const auto next = !isRightToLeft;
if (CheckAndJumpToNearChatsFilter(controller(), next, false)) {
_swipeBackMirrored = !scrollRightToLeft;
return Ui::Controls::DefaultSwipeBackHandlerFinishData([=] {
_swipeBackData = {};
CheckAndJumpToNearChatsFilter(controller(), next, true);
});
}
}
// With a quick action other than moving between the chats filters the
// swipe never moves between them at all, so the main menu is not
// waiting past their end - it is dragged out from any of them.
const auto mainMenuHere = [=] {
return !isDisabled || noNearChatsFilter(next);
};
if (isRightToLeft && mainMenuHere()) {
_swipeBackIconMirrored = true;
return Ui::Controls::DefaultSwipeBackHandlerFinishData([=] {
_swipeBackIconMirrored = false;
_swipeBackData = {};
if (mainMenuHere()) {
showMainMenu();
}
});
}
return Ui::Controls::SwipeHandlerFinishData();
};

View File

@@ -476,8 +476,8 @@ void PaintRow(
&& !context.quickActionContext->ripple
&& (history->peer->id.value
== context.quickActionContext->data.msgBareId)) {
swipeTranslation = context.quickActionContext->data.exactTranslation
* -2;
swipeTranslation
= context.quickActionContext->data.visualExactTranslation() * -2;
}
if (swipeTranslation) {
p.translate(-swipeTranslation, 0);

View File

@@ -1707,15 +1707,14 @@ void HistoryInner::paintEvent(QPaintEvent *e) {
const auto hasTranslation = context.gestureHorizontal.translation
&& (context.gestureHorizontal.msgBareId
== item->fullId().msg.bare);
const auto shift = context.gestureHorizontal.visualTranslation();
if (hasTranslation) {
p.translate(context.gestureHorizontal.translation, 0);
p.translate(shift, 0);
update(
QRect(
st::historyPhotoLeft
+ context.gestureHorizontal.translation,
st::historyPhotoLeft + std::min(shift, 0),
userpicTop,
st::msgPhotoSize
- context.gestureHorizontal.translation,
st::msgPhotoSize + std::abs(shift),
st::msgPhotoSize));
}
if (const auto from = item->displayFrom()) {
@@ -1754,7 +1753,7 @@ void HistoryInner::paintEvent(QPaintEvent *e) {
Unexpected("Corrupt forwarded information in message.");
}
if (hasTranslation) {
p.translate(-_gestureHorizontal.translation, 0);
p.translate(-shift, 0);
}
}
return true;

View File

@@ -3241,15 +3241,14 @@ void ListWidget::paintUserpics(
const auto hasTranslation = context.gestureHorizontal.translation
&& (context.gestureHorizontal.msgBareId
== item->fullId().msg.bare);
const auto shift = context.gestureHorizontal.visualTranslation();
if (hasTranslation) {
p.translate(context.gestureHorizontal.translation, 0);
p.translate(shift, 0);
update(
QRect(
st::historyPhotoLeft
+ context.gestureHorizontal.translation,
st::historyPhotoLeft + std::min(shift, 0),
userpicTop,
st::msgPhotoSize
- context.gestureHorizontal.translation,
st::msgPhotoSize + std::abs(shift),
st::msgPhotoSize));
}
if (const auto from = view->displayFrom()) {
@@ -3288,7 +3287,7 @@ void ListWidget::paintUserpics(
Unexpected("Corrupt forwarded information in message.");
}
if (hasTranslation) {
p.translate(-context.gestureHorizontal.translation, 0);
p.translate(-shift, 0);
}
}
return true;

View File

@@ -1723,8 +1723,9 @@ void Message::draw(Painter &p, const PaintContext &context) const {
const auto hasGesture = context.gestureHorizontal.translation
&& (context.gestureHorizontal.msgBareId == item->fullId().msg.bare);
const auto gestureShift = context.gestureHorizontal.visualTranslation();
if (hasGesture) {
p.translate(context.gestureHorizontal.translation, 0);
p.translate(gestureShift, 0);
}
const auto selectionModeResult = delegate()->elementInSelectionMode(this);
const auto selectionTranslation = (selectionModeResult.progress > 0)
@@ -2228,10 +2229,9 @@ void Message::draw(Painter &p, const PaintContext &context) const {
}
}
if (hasGesture) {
p.translate(-context.gestureHorizontal.translation, 0);
p.translate(-gestureShift, 0);
if (context.reactionInfo && context.reactionInfo->effectPaint) {
const auto shift = context.gestureHorizontal.translation;
context.reactionInfo->effectOffset += QPoint(shift, 0);
context.reactionInfo->effectOffset += QPoint(gestureShift, 0);
}
constexpr auto kShiftRatio = 1.5;
@@ -2239,13 +2239,17 @@ void Message::draw(Painter &p, const PaintContext &context) const {
constexpr auto kMaxHeightRatio = 3.5;
constexpr auto kStrokeWidth = 2.;
constexpr auto kWaveWidth = 10.;
const auto mirrored = !context.gestureHorizontal.inverted;
const auto isLeftSize = !context.outbg
|| (delegate()->elementChatMode() == ElementChatMode::Wide);
const auto ratio = std::min(context.gestureHorizontal.ratio, 1.);
const auto reachRatio = context.gestureHorizontal.reachRatio;
const auto size = st::historyFastShareSize;
const auto bubbleRight = mirrored
? (width() - g.x())
: rect::right(g);
const auto outerWidth = st::historySwipeIconSkip
+ (isLeftSize ? rect::right(g) : width())
+ (isLeftSize ? bubbleRight : width())
+ ((g.height() < size * kMaxHeightRatio)
? rightActionSize().value_or(QSize()).width()
: 0);
@@ -2274,6 +2278,10 @@ void Message::draw(Painter &p, const PaintContext &context) const {
pen.setWidthF(strokeWidth - (1. * (reachScale / kBouncePart)));
const auto arcRect = rect - Margins(strokeWidth);
p.save();
if (mirrored) {
p.translate(width(), 0);
p.scale(-1., 1.);
}
{
auto hq = PainterHighQualityEnabler(p);
p.setPen(Qt::NoPen);

View File

@@ -88,6 +88,7 @@ void SetupSwipeHandler(SwipeHandlerArgs &&args) {
QPointF position;
QPointF delta;
bool touch = false;
bool inverted = false;
};
struct State {
base::unique_qptr<QObject> filter;
@@ -107,6 +108,7 @@ void SetupSwipeHandler(SwipeHandlerArgs &&args) {
bool started = false;
bool reached = false;
bool touch = false;
bool inverted = false;
rpl::lifetime lifetime;
};
@@ -147,6 +149,7 @@ void SetupSwipeHandler(SwipeHandlerArgs &&args) {
state->data.exactTranslation = exactTranslation
* state->directionInt;
state->data.cursorTop = state->cursorPosition.y();
state->data.inverted = state->inverted;
update(state->data);
};
const auto setOrientation = [=](std::optional<Qt::Orientation> o) {
@@ -207,6 +210,7 @@ void SetupSwipeHandler(SwipeHandlerArgs &&args) {
update(state->data);
};
const auto updateWith = [=, generateFinish = args.init](UpdateArgs args) {
state->inverted = args.inverted;
const auto fillFinishByTop = [&] {
if (!args.delta.x()) {
return;
@@ -220,6 +224,7 @@ void SetupSwipeHandler(SwipeHandlerArgs &&args) {
state->finishByTopData = generateFinish({
.cursorPosition = state->cursorPosition,
.direction = *state->direction,
.inverted = state->inverted,
});
state->threshold = style::ConvertFloatScale(kThresholdWidth)
* state->finishByTopData.speedRatio;
@@ -352,6 +357,7 @@ void SetupSwipeHandler(SwipeHandlerArgs &&args) {
.position = touches[0].pos(),
.delta = state->startAt - touches[0].pos(),
.touch = true,
.inverted = true,
};
updateWith(args);
}
@@ -378,13 +384,12 @@ void SetupSwipeHandler(SwipeHandlerArgs &&args) {
if (cancel) {
processEnd();
} else {
const auto invert = (w->inverted() ? -1 : 1);
const auto delta = Ui::ScrollDeltaF(w) * invert;
updateWith({
.globalCursor = w->globalPosition().toPoint(),
.position = QPointF(),
.delta = state->delta + delta * kSwipeSlow,
.delta = state->delta - Ui::ScrollDeltaF(w) * kSwipeSlow,
.touch = false,
.inverted = w->inverted(),
});
}
} break;

View File

@@ -20,17 +20,44 @@ struct SwipeContextData final {
return !empty();
}
// The system reports the gesture deltas in the scroll direction, so with
// the natural scrolling turned off they are the opposite of the finger
// movement. The translation sign follows the gesture and tells which of
// the two actions was started, while the content should follow the
// finger, so these give the direction to paint it moving in.
[[nodiscard]] int visualTranslation() const {
return inverted ? translation : -translation;
}
[[nodiscard]] float64 visualExactTranslation() const {
return inverted ? exactTranslation : -exactTranslation;
}
float64 ratio = 0.;
float64 reachRatio = 0.;
float64 exactTranslation = 0.;
int64 msgBareId = 0;
int translation = 0;
int cursorTop = 0;
bool inverted = false;
};
struct SwipeHandlerInitData final {
// The direction follows the scroll, which is what a gesture that just
// moves between two equal things should use. A gesture that drags out
// something living at a fixed side of the window, like the main menu,
// has to be started by moving the finger towards that side instead, so
// it should look at this one and stay the same with any scroll setting.
[[nodiscard]] Qt::LayoutDirection fingerDirection() const {
return inverted
? direction
: ((direction == Qt::LeftToRight)
? Qt::RightToLeft
: Qt::LeftToRight);
}
QPoint cursorPosition;
Qt::LayoutDirection direction = Qt::LeftToRight;
bool inverted = false;
};
struct SwipeBackResult final {

View File

@@ -1011,7 +1011,7 @@ void MainMenu::setupSwipe() {
}
auto update = [=](Ui::Controls::SwipeContextData data) {
if (data.translation < 0) {
if (data.visualTranslation() < 0) {
if (!_swipeBackData.callback) {
_swipeBackData = Ui::Controls::SetupSwipeBack(
this,
@@ -1030,7 +1030,7 @@ void MainMenu::setupSwipe() {
};
auto init = [=](Ui::Controls::SwipeHandlerInitData data) {
if (data.direction != Qt::LeftToRight) {
if (data.fingerDirection() != Qt::LeftToRight) {
return Ui::Controls::SwipeHandlerFinishData();
}
if (_emojiStatusPanel && _emojiStatusPanel->hasFocus()) {