swipe_handler.cpp 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532
  1. /*
  2. This file is part of Telegram Desktop,
  3. the official desktop application for the Telegram messaging service.
  4. For license and copyright information please follow this link:
  5. https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
  6. */
  7. #include "ui/controls/swipe_handler.h"
  8. #include "base/debug_log.h"
  9. #include "base/platform/base_platform_haptic.h"
  10. #include "base/platform/base_platform_info.h"
  11. #include "base/qt/qt_common_adapters.h"
  12. #include "base/event_filter.h"
  13. #include "ui/chat/chat_style.h"
  14. #include "ui/controls/swipe_handler_data.h"
  15. #include "ui/painter.h"
  16. #include "ui/rect.h"
  17. #include "ui/ui_utility.h"
  18. #include "ui/widgets/elastic_scroll.h"
  19. #include "ui/widgets/scroll_area.h"
  20. #include "styles/style_chat.h"
  21. #include <QtWidgets/QApplication>
  22. namespace Ui::Controls {
  23. namespace {
  24. constexpr auto kSwipeSlow = 0.2;
  25. constexpr auto kMsgBareIdSwipeBack = std::numeric_limits<int64>::max() - 77;
  26. constexpr auto kSwipedBackSpeedRatio = 0.35;
  27. float64 InterpolationRatio(float64 from, float64 to, float64 result) {
  28. return (result - from) / (to - from);
  29. };
  30. class RatioRange final {
  31. public:
  32. [[nodiscard]] float64 calcRatio(float64 value) {
  33. if (value < _min) {
  34. const auto shift = _min - value;
  35. _min -= shift;
  36. _max -= shift;
  37. _max = _min + 1;
  38. } else if (value > _max) {
  39. const auto shift = value - _max;
  40. _min += shift;
  41. _max += shift;
  42. _max = _min + 1;
  43. }
  44. return InterpolationRatio(_min, _max, value);
  45. }
  46. private:
  47. float64 _min = 0;
  48. float64 _max = 1;
  49. };
  50. } // namespace
  51. void SetupSwipeHandler(
  52. not_null<Ui::RpWidget*> widget,
  53. Scroll scroll,
  54. Fn<void(SwipeContextData)> update,
  55. Fn<SwipeHandlerFinishData(int, Qt::LayoutDirection)> generateFinish,
  56. rpl::producer<bool> dontStart,
  57. rpl::lifetime *onLifetime) {
  58. static constexpr auto kThresholdWidth = 50;
  59. static constexpr auto kMaxRatio = 1.5;
  60. struct UpdateArgs {
  61. QPoint globalCursor;
  62. QPointF position;
  63. QPointF delta;
  64. bool touch = false;
  65. };
  66. struct State {
  67. base::unique_qptr<QObject> filter;
  68. Ui::Animations::Simple animationReach;
  69. Ui::Animations::Simple animationEnd;
  70. SwipeContextData data;
  71. SwipeHandlerFinishData finishByTopData;
  72. std::optional<Qt::Orientation> orientation;
  73. std::optional<Qt::LayoutDirection> direction;
  74. float64 threshold = style::ConvertFloatScale(kThresholdWidth);
  75. RatioRange ratioRange;
  76. int directionInt = 1.;
  77. QPointF startAt;
  78. QPointF delta;
  79. int cursorTop = 0;
  80. bool dontStart = false;
  81. bool started = false;
  82. bool reached = false;
  83. bool touch = false;
  84. rpl::lifetime lifetime;
  85. };
  86. auto &useLifetime = onLifetime ? *onLifetime : widget->lifetime();
  87. const auto state = useLifetime.make_state<State>();
  88. if (dontStart) {
  89. std::move(
  90. dontStart
  91. ) | rpl::start_with_next([=](bool dontStart) {
  92. state->dontStart = dontStart;
  93. }, state->lifetime);
  94. } else {
  95. v::match(scroll, [](v::null_t) {
  96. }, [&](const auto &scroll) {
  97. scroll->touchMaybePressing(
  98. ) | rpl::start_with_next([=](bool maybePressing) {
  99. state->dontStart = maybePressing;
  100. }, state->lifetime);
  101. });
  102. }
  103. const auto updateRatio = [=](float64 ratio) {
  104. ratio = std::max(ratio, 0.);
  105. state->data.ratio = ratio;
  106. const auto overscrollRatio = std::max(ratio - 1., 0.);
  107. const auto translation = int(
  108. base::SafeRound(-std::min(ratio, 1.) * state->threshold)
  109. ) + Ui::OverscrollFromAccumulated(int(
  110. base::SafeRound(-overscrollRatio * state->threshold)
  111. ));
  112. state->data.msgBareId = state->finishByTopData.msgBareId;
  113. state->data.translation = translation
  114. * state->directionInt;
  115. state->data.cursorTop = state->cursorTop;
  116. update(state->data);
  117. };
  118. const auto setOrientation = [=](std::optional<Qt::Orientation> o) {
  119. state->orientation = o;
  120. const auto isHorizontal = (o == Qt::Horizontal);
  121. v::match(scroll, [](v::null_t) {
  122. }, [&](const auto &scroll) {
  123. if (const auto viewport = scroll->viewport()) {
  124. if (viewport != widget) {
  125. viewport->setAttribute(
  126. Qt::WA_AcceptTouchEvents,
  127. !isHorizontal);
  128. }
  129. }
  130. scroll->disableScroll(isHorizontal);
  131. });
  132. };
  133. const auto processEnd = [=](std::optional<QPointF> delta = {}) {
  134. if (state->orientation == Qt::Horizontal) {
  135. const auto rawRatio = delta.value_or(state->delta).x()
  136. / state->threshold
  137. * state->directionInt;
  138. const auto ratio = std::clamp(
  139. state->finishByTopData.keepRatioWithinRange
  140. ? state->ratioRange.calcRatio(rawRatio)
  141. : rawRatio,
  142. 0.,
  143. kMaxRatio);
  144. if ((ratio >= 1) && state->finishByTopData.callback) {
  145. Ui::PostponeCall(
  146. widget,
  147. state->finishByTopData.callback);
  148. }
  149. state->animationEnd.stop();
  150. state->animationEnd.start(
  151. updateRatio,
  152. ratio,
  153. 0.,
  154. std::min(1., ratio) * st::slideWrapDuration);
  155. }
  156. setOrientation(std::nullopt);
  157. state->started = false;
  158. state->reached = false;
  159. state->direction = std::nullopt;
  160. state->startAt = {};
  161. state->delta = {};
  162. };
  163. v::match(scroll, [](v::null_t) {
  164. }, [&](const auto &scroll) {
  165. scroll->scrolls() | rpl::start_with_next([=] {
  166. if (state->orientation != Qt::Vertical) {
  167. processEnd();
  168. }
  169. }, state->lifetime);
  170. });
  171. const auto animationReachCallback = [=](float64 value) {
  172. state->data.reachRatio = value;
  173. update(state->data);
  174. };
  175. const auto updateWith = [=](UpdateArgs args) {
  176. const auto fillFinishByTop = [&] {
  177. if (!args.delta.x()) {
  178. LOG(("SKIPPING fillFinishByTop."));
  179. return;
  180. }
  181. LOG(("SETTING DIRECTION"));
  182. state->direction = (args.delta.x() < 0)
  183. ? Qt::RightToLeft
  184. : Qt::LeftToRight;
  185. state->directionInt = (state->direction == Qt::LeftToRight)
  186. ? 1
  187. : -1;
  188. state->finishByTopData = generateFinish(
  189. state->cursorTop,
  190. *state->direction);
  191. state->threshold = style::ConvertFloatScale(kThresholdWidth)
  192. * state->finishByTopData.speedRatio;
  193. if (!state->finishByTopData.callback) {
  194. setOrientation(Qt::Vertical);
  195. }
  196. };
  197. if (!state->started || state->touch != args.touch) {
  198. LOG(("STARTING"));
  199. state->started = true;
  200. state->data.reachRatio = 0.;
  201. state->touch = args.touch;
  202. state->startAt = args.position;
  203. state->cursorTop = widget->mapFromGlobal(args.globalCursor).y();
  204. if (!state->touch) {
  205. // args.delta already is valid.
  206. fillFinishByTop();
  207. } else {
  208. // args.delta depends on state->startAt, so it's invalid.
  209. state->direction = std::nullopt;
  210. }
  211. state->delta = QPointF();
  212. } else if (!state->direction) {
  213. fillFinishByTop();
  214. } else if (!state->orientation) {
  215. state->delta = args.delta;
  216. const auto diffXtoY = std::abs(args.delta.x())
  217. - std::abs(args.delta.y());
  218. constexpr auto kOrientationThreshold = 1.;
  219. LOG(("SETTING ORIENTATION WITH: %1,%2, diff %3"
  220. ).arg(args.delta.x()
  221. ).arg(args.delta.y()
  222. ).arg(diffXtoY));
  223. if (diffXtoY > kOrientationThreshold) {
  224. if (!state->dontStart) {
  225. setOrientation(Qt::Horizontal);
  226. }
  227. } else if (diffXtoY < -kOrientationThreshold) {
  228. setOrientation(Qt::Vertical);
  229. } else {
  230. setOrientation(std::nullopt);
  231. }
  232. } else if (*state->orientation == Qt::Horizontal) {
  233. state->delta = args.delta;
  234. const auto rawRatio = 0
  235. + args.delta.x() * state->directionInt / state->threshold;
  236. const auto ratio = state->finishByTopData.keepRatioWithinRange
  237. ? state->ratioRange.calcRatio(rawRatio)
  238. : rawRatio;
  239. updateRatio(ratio);
  240. constexpr auto kResetReachedOn = 0.95;
  241. constexpr auto kBounceDuration = crl::time(500);
  242. if (!state->reached && ratio >= 1.) {
  243. state->reached = true;
  244. state->animationReach.stop();
  245. state->animationReach.start(
  246. animationReachCallback,
  247. 0.,
  248. 1.,
  249. state->finishByTopData.reachRatioDuration
  250. ? state->finishByTopData.reachRatioDuration
  251. : kBounceDuration);
  252. base::Platform::Haptic();
  253. } else if (state->reached
  254. && ratio < kResetReachedOn) {
  255. if (state->finishByTopData.provideReachOutRatio) {
  256. state->animationReach.stop();
  257. state->animationReach.start(
  258. animationReachCallback,
  259. 1.,
  260. 0.,
  261. state->finishByTopData.reachRatioDuration
  262. ? state->finishByTopData.reachRatioDuration
  263. : kBounceDuration);
  264. }
  265. state->reached = false;
  266. }
  267. }
  268. };
  269. const auto filter = [=](not_null<QEvent*> e) {
  270. if (!widget->testAttribute(Qt::WA_AcceptTouchEvents)) {
  271. [[maybe_unused]] int a = 0;
  272. }
  273. const auto type = e->type();
  274. switch (type) {
  275. case QEvent::Leave: {
  276. if (state->orientation == Qt::Horizontal) {
  277. processEnd();
  278. }
  279. } break;
  280. case QEvent::MouseMove: {
  281. if (state->orientation == Qt::Horizontal) {
  282. const auto m = static_cast<QMouseEvent*>(e.get());
  283. if (std::abs(m->pos().y() - state->cursorTop)
  284. > QApplication::startDragDistance()) {
  285. processEnd();
  286. }
  287. }
  288. } break;
  289. case QEvent::TouchBegin:
  290. case QEvent::TouchUpdate:
  291. case QEvent::TouchEnd:
  292. case QEvent::TouchCancel: {
  293. const auto t = static_cast<QTouchEvent*>(e.get());
  294. const auto touchscreen = t->device()
  295. && (t->device()->type() == base::TouchDevice::TouchScreen);
  296. if (!touchscreen && type != QEvent::TouchCancel) {
  297. break;
  298. } else if (type == QEvent::TouchBegin) {
  299. // Reset state in case we lost some TouchEnd.
  300. processEnd();
  301. }
  302. const auto &touches = t->touchPoints();
  303. const auto released = [&](int index) {
  304. return (touches.size() > index)
  305. && (int(touches.at(index).state())
  306. & int(Qt::TouchPointReleased));
  307. };
  308. const auto cancel = released(0)
  309. || released(1)
  310. || (touches.size() != (touchscreen ? 1 : 2))
  311. || (type == QEvent::TouchEnd)
  312. || (type == QEvent::TouchCancel);
  313. if (cancel) {
  314. processEnd(touches.empty()
  315. ? std::optional<QPointF>()
  316. : (state->startAt - touches[0].pos()));
  317. } else {
  318. const auto args = UpdateArgs{
  319. .globalCursor = (touchscreen
  320. ? touches[0].screenPos().toPoint()
  321. : QCursor::pos()),
  322. .position = touches[0].pos(),
  323. .delta = state->startAt - touches[0].pos(),
  324. .touch = true,
  325. };
  326. LOG(("ORIENTATION UPDATING WITH: %1, %2").arg(args.delta.x()).arg(args.delta.y()));
  327. updateWith(args);
  328. }
  329. LOG(("ORIENTATION: %1").arg(!state->orientation ? "none" : (state->orientation == Qt::Horizontal) ? "horizontal" : "vertical"));
  330. return (touchscreen && state->orientation != Qt::Horizontal)
  331. ? base::EventFilterResult::Continue
  332. : base::EventFilterResult::Cancel;
  333. } break;
  334. case QEvent::Wheel: {
  335. const auto w = static_cast<QWheelEvent*>(e.get());
  336. const auto phase = w->phase();
  337. if (phase == Qt::NoScrollPhase) {
  338. break;
  339. } else if (phase == Qt::ScrollBegin) {
  340. // Reset state in case we lost some TouchEnd.
  341. processEnd();
  342. }
  343. const auto cancel = w->buttons()
  344. || (phase == Qt::ScrollEnd)
  345. || (phase == Qt::ScrollMomentum);
  346. if (cancel) {
  347. processEnd();
  348. } else {
  349. const auto invert = (w->inverted() ? -1 : 1);
  350. const auto delta = Ui::ScrollDeltaF(w) * invert;
  351. updateWith({
  352. .globalCursor = w->globalPosition().toPoint(),
  353. .position = QPointF(),
  354. .delta = state->delta + delta * kSwipeSlow,
  355. .touch = false,
  356. });
  357. }
  358. } break;
  359. }
  360. return base::EventFilterResult::Continue;
  361. };
  362. widget->setAttribute(Qt::WA_AcceptTouchEvents);
  363. state->filter = base::make_unique_q<QObject>(
  364. base::install_event_filter(widget, filter));
  365. }
  366. SwipeBackResult SetupSwipeBack(
  367. not_null<Ui::RpWidget*> widget,
  368. Fn<std::pair<QColor, QColor>()> colors,
  369. bool mirrored,
  370. bool iconMirrored) {
  371. struct State {
  372. base::unique_qptr<Ui::RpWidget> back;
  373. SwipeContextData data;
  374. };
  375. constexpr auto kMaxInnerOffset = 0.5;
  376. constexpr auto kMaxOuterOffset = 0.8;
  377. constexpr auto kIdealSize = 100;
  378. const auto maxOffset = st::swipeBackSize * kMaxInnerOffset;
  379. const auto sizeRatio = st::swipeBackSize
  380. / style::ConvertFloatScale(kIdealSize);
  381. auto lifetime = rpl::lifetime();
  382. const auto state = lifetime.make_state<State>();
  383. const auto paintCallback = [=] {
  384. const auto [bg, fg] = colors();
  385. const auto arrowPen = QPen(
  386. fg,
  387. st::lineWidth * 3 * sizeRatio,
  388. Qt::SolidLine,
  389. Qt::RoundCap);
  390. return [=] {
  391. auto p = QPainter(state->back);
  392. constexpr auto kBouncePart = 0.25;
  393. constexpr auto kStrokeWidth = 2.;
  394. constexpr auto kWaveWidth = 10.;
  395. const auto ratio = std::min(state->data.ratio, 1.);
  396. const auto reachRatio = state->data.reachRatio;
  397. const auto rect = state->back->rect()
  398. - Margins(state->back->width() / 4);
  399. const auto center = rect::center(rect);
  400. const auto strokeWidth = style::ConvertFloatScale(kStrokeWidth)
  401. * sizeRatio;
  402. const auto reachScale = std::clamp(
  403. (reachRatio > kBouncePart)
  404. ? (kBouncePart * 2 - reachRatio)
  405. : reachRatio,
  406. 0.,
  407. 1.);
  408. auto pen = QPen(bg);
  409. pen.setWidthF(strokeWidth - (1. * (reachScale / kBouncePart)));
  410. const auto arcRect = rect - Margins(strokeWidth);
  411. auto hq = PainterHighQualityEnabler(p);
  412. p.setOpacity(ratio);
  413. if (reachScale || mirrored) {
  414. const auto scale = (1. + 1. * reachScale);
  415. p.translate(center);
  416. p.scale(scale * (mirrored ? -1 : 1), scale);
  417. p.translate(-center);
  418. }
  419. {
  420. p.setPen(Qt::NoPen);
  421. p.setBrush(bg);
  422. p.drawEllipse(rect);
  423. p.drawEllipse(rect);
  424. p.setPen(arrowPen);
  425. p.setBrush(Qt::NoBrush);
  426. const auto halfSize = rect.width() / 2;
  427. const auto arrowSize = halfSize / 2;
  428. const auto arrowHalf = arrowSize / 2;
  429. const auto arrowX = st::swipeBackSize / 8
  430. + rect.x()
  431. + halfSize;
  432. const auto arrowY = rect.y() + halfSize;
  433. auto arrowPath = QPainterPath();
  434. const auto direction = iconMirrored ? -1 : 1;
  435. arrowPath.moveTo(arrowX + direction * arrowSize, arrowY);
  436. arrowPath.lineTo(arrowX, arrowY);
  437. arrowPath.lineTo(
  438. arrowX + direction * arrowHalf,
  439. arrowY - arrowHalf);
  440. arrowPath.moveTo(arrowX, arrowY);
  441. arrowPath.lineTo(
  442. arrowX + direction * arrowHalf,
  443. arrowY + arrowHalf);
  444. arrowPath.translate(-direction * arrowHalf, 0);
  445. p.drawPath(arrowPath);
  446. }
  447. if (reachRatio) {
  448. p.setPen(pen);
  449. p.setBrush(Qt::NoBrush);
  450. const auto w = style::ConvertFloatScale(kWaveWidth)
  451. * sizeRatio;
  452. p.setOpacity(ratio - reachRatio);
  453. p.drawArc(
  454. arcRect + Margins(reachRatio * reachRatio * w),
  455. arc::kQuarterLength,
  456. arc::kFullLength);
  457. }
  458. };
  459. };
  460. const auto callback = ([=](SwipeContextData data) {
  461. const auto ratio = std::min(1.0, data.ratio);
  462. state->data = std::move(data);
  463. if (ratio > 0) {
  464. if (!state->back) {
  465. state->back = base::make_unique_q<Ui::RpWidget>(widget);
  466. const auto raw = state->back.get();
  467. raw->paintRequest(
  468. ) | rpl::start_with_next(paintCallback(), raw->lifetime());
  469. raw->setAttribute(Qt::WA_TransparentForMouseEvents);
  470. raw->resize(Size(st::swipeBackSize));
  471. raw->show();
  472. raw->raise();
  473. }
  474. if (!mirrored) {
  475. state->back->moveToLeft(
  476. anim::interpolate(
  477. -st::swipeBackSize * kMaxOuterOffset,
  478. maxOffset - st::swipeBackSize,
  479. ratio),
  480. (widget->height() - state->back->height()) / 2);
  481. } else {
  482. state->back->moveToLeft(
  483. anim::interpolate(
  484. widget->width() + st::swipeBackSize * kMaxOuterOffset,
  485. widget->width() - maxOffset,
  486. ratio),
  487. (widget->height() - state->back->height()) / 2);
  488. }
  489. state->back->update();
  490. } else if (state->back) {
  491. state->back = nullptr;
  492. }
  493. });
  494. return { std::move(lifetime), std::move(callback) };
  495. }
  496. SwipeHandlerFinishData DefaultSwipeBackHandlerFinishData(
  497. Fn<void(void)> callback) {
  498. return {
  499. .callback = std::move(callback),
  500. .msgBareId = kMsgBareIdSwipeBack,
  501. .speedRatio = kSwipedBackSpeedRatio,
  502. .keepRatioWithinRange = true,
  503. };
  504. }
  505. } // namespace Ui::Controls