vertical_drum_picker.cpp 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289
  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/widgets/vertical_drum_picker.h"
  8. #include "ui/effects/animation_value_f.h"
  9. #include "ui/ui_utility.h"
  10. #include "styles/style_basic.h"
  11. namespace Ui {
  12. namespace {
  13. constexpr auto kAlmostIndex = float64(.99);
  14. } // namespace
  15. PickerAnimation::PickerAnimation() = default;
  16. void PickerAnimation::jumpToOffset(int offset) {
  17. _result.from = _result.current;
  18. _result.to += offset;
  19. _animation.stop();
  20. auto callback = [=](float64 value) {
  21. const auto was = _result.current;
  22. _result.current = anim::interpolateF(
  23. _result.from,
  24. _result.to,
  25. value);
  26. _updates.fire(_result.current - was);
  27. };
  28. if (anim::Disabled()) {
  29. auto value = float64(0.);
  30. const auto diff = _result.to - _result.from;
  31. const auto step = std::min(
  32. kAlmostIndex,
  33. 1. / (std::max(1. - kAlmostIndex, std::abs(diff) + 1)));
  34. while (true) {
  35. value += step;
  36. if (value >= 1.) {
  37. callback(1.);
  38. break;
  39. } else {
  40. callback(value);
  41. }
  42. }
  43. return;
  44. }
  45. _animation.start(
  46. std::move(callback),
  47. 0.,
  48. 1.,
  49. st::fadeWrapDuration);
  50. }
  51. void PickerAnimation::setResult(float64 from, float64 current, float64 to) {
  52. _result = { from, current, to };
  53. }
  54. rpl::producer<PickerAnimation::Shift> PickerAnimation::updates() const {
  55. return _updates.events();
  56. }
  57. VerticalDrumPicker::VerticalDrumPicker(
  58. not_null<Ui::RpWidget*> parent,
  59. PaintItemCallback &&paintCallback,
  60. int itemsCount,
  61. int itemHeight,
  62. int startIndex,
  63. bool looped)
  64. : RpWidget(parent)
  65. , _itemsCount(itemsCount)
  66. , _itemHeight(itemHeight)
  67. , _paintCallback(std::move(paintCallback))
  68. , _pendingStartIndex(startIndex)
  69. , _loopData({ .looped = looped }) {
  70. Expects(_paintCallback != nullptr);
  71. sizeValue(
  72. ) | rpl::start_with_next([=](const QSize &s) {
  73. _itemsVisible.count = std::ceil(float64(s.height()) / _itemHeight);
  74. _itemsVisible.centerOffset = _itemsVisible.count / 2;
  75. if ((_pendingStartIndex >= 0) && _itemsVisible.count) {
  76. _index = normalizedIndex(_pendingStartIndex
  77. - _itemsVisible.centerOffset);
  78. _pendingStartIndex = -1;
  79. }
  80. if (!_loopData.looped) {
  81. _loopData.minIndex = -_itemsVisible.centerOffset;
  82. _loopData.maxIndex = _itemsCount - 1 - _itemsVisible.centerOffset;
  83. }
  84. _changes.fire({});
  85. }, lifetime());
  86. paintRequest(
  87. ) | rpl::start_with_next([=] {
  88. auto p = QPainter(this);
  89. const auto outerWidth = width();
  90. const auto centerY = height() / 2.;
  91. const auto shiftedY = _itemHeight * _shift;
  92. for (auto i = -1; i < (_itemsVisible.count + 1); i++) {
  93. const auto index = normalizedIndex(i + _index);
  94. if (!isIndexInRange(index)) {
  95. continue;
  96. }
  97. const auto y = (_itemHeight * i + shiftedY);
  98. _paintCallback(
  99. p,
  100. index,
  101. y,
  102. ((y + _itemHeight / 2.) - centerY) / centerY,
  103. outerWidth);
  104. }
  105. }, lifetime());
  106. _animation.updates(
  107. ) | rpl::start_with_next([=](PickerAnimation::Shift shift) {
  108. increaseShift(shift);
  109. }, lifetime());
  110. }
  111. void VerticalDrumPicker::increaseShift(float64 by) {
  112. {
  113. // Guard input.
  114. if (by >= 1.) {
  115. by = kAlmostIndex;
  116. } else if (by <= -1.) {
  117. by = -kAlmostIndex;
  118. }
  119. }
  120. auto shift = _shift;
  121. auto index = _index;
  122. shift += by;
  123. if (shift >= 1.) {
  124. shift -= 1.;
  125. index--;
  126. index = normalizedIndex(index);
  127. } else if (shift <= -1.) {
  128. shift += 1.;
  129. index++;
  130. index = normalizedIndex(index);
  131. }
  132. if (_loopData.minIndex == _loopData.maxIndex) {
  133. _shift = 0.;
  134. } else if (!_loopData.looped && (index <= _loopData.minIndex)) {
  135. _shift = std::min(0., shift);
  136. _index = _loopData.minIndex;
  137. } else if (!_loopData.looped && (index >= _loopData.maxIndex)) {
  138. _shift = std::max(0., shift);
  139. _index = _loopData.maxIndex;
  140. } else {
  141. _shift = shift;
  142. _index = index;
  143. }
  144. _changes.fire({});
  145. update();
  146. }
  147. void VerticalDrumPicker::handleWheelEvent(not_null<QWheelEvent*> e) {
  148. const auto direction = Ui::WheelDirection(e);
  149. if (direction) {
  150. _animation.jumpToOffset(direction);
  151. } else {
  152. if (const auto delta = e->pixelDelta().y(); delta) {
  153. increaseShift(delta / float64(_itemHeight));
  154. } else if (e->phase() == Qt::ScrollEnd) {
  155. animationDataFromIndex();
  156. _animation.jumpToOffset(0);
  157. } else {
  158. constexpr auto step = int(QWheelEvent::DefaultDeltasPerStep);
  159. _touch.verticalDelta += e->angleDelta().y();
  160. while (std::abs(_touch.verticalDelta) >= step) {
  161. if (_touch.verticalDelta < 0) {
  162. _touch.verticalDelta += step;
  163. _animation.jumpToOffset(1);
  164. } else {
  165. _touch.verticalDelta -= step;
  166. _animation.jumpToOffset(-1);
  167. }
  168. }
  169. }
  170. }
  171. }
  172. void VerticalDrumPicker::handleKeyEvent(not_null<QKeyEvent*> e) {
  173. if (e->key() == Qt::Key_Left || e->key() == Qt::Key_Up) {
  174. _animation.jumpToOffset(1);
  175. } else if (e->key() == Qt::Key_PageUp && !e->isAutoRepeat()) {
  176. _animation.jumpToOffset(_itemsVisible.count);
  177. } else if (e->key() == Qt::Key_Right || e->key() == Qt::Key_Down) {
  178. _animation.jumpToOffset(-1);
  179. } else if (e->key() == Qt::Key_PageDown && !e->isAutoRepeat()) {
  180. _animation.jumpToOffset(-_itemsVisible.count);
  181. }
  182. }
  183. void VerticalDrumPicker::handleMouseEvent(not_null<QMouseEvent*> e) {
  184. if (e->type() == QEvent::MouseButtonPress) {
  185. _mouse.pressed = true;
  186. _mouse.lastPositionY = e->pos().y();
  187. } else if (e->type() == QEvent::MouseMove) {
  188. if (_mouse.pressed) {
  189. const auto was = _mouse.lastPositionY;
  190. _mouse.lastPositionY = e->pos().y();
  191. const auto diff = _mouse.lastPositionY - was;
  192. increaseShift(float64(diff) / _itemHeight);
  193. _mouse.clickDisabled = true;
  194. }
  195. } else if (e->type() == QEvent::MouseButtonRelease) {
  196. if (_mouse.clickDisabled) {
  197. animationDataFromIndex();
  198. _animation.jumpToOffset(0);
  199. } else {
  200. _mouse.lastPositionY = e->pos().y();
  201. const auto toOffset = _itemsVisible.centerOffset
  202. - (_mouse.lastPositionY / _itemHeight);
  203. _animation.jumpToOffset(toOffset);
  204. }
  205. _mouse = {};
  206. }
  207. }
  208. void VerticalDrumPicker::wheelEvent(QWheelEvent *e) {
  209. handleWheelEvent(e);
  210. }
  211. void VerticalDrumPicker::mousePressEvent(QMouseEvent *e) {
  212. handleMouseEvent(e);
  213. }
  214. void VerticalDrumPicker::mouseMoveEvent(QMouseEvent *e) {
  215. handleMouseEvent(e);
  216. }
  217. void VerticalDrumPicker::mouseReleaseEvent(QMouseEvent *e) {
  218. handleMouseEvent(e);
  219. }
  220. void VerticalDrumPicker::keyPressEvent(QKeyEvent *e) {
  221. handleKeyEvent(e);
  222. }
  223. void VerticalDrumPicker::animationDataFromIndex() {
  224. _animation.setResult(
  225. _index,
  226. _index + _shift,
  227. std::round(_index + _shift));
  228. }
  229. bool VerticalDrumPicker::isIndexInRange(int index) const {
  230. return (index >= 0) && (index < _itemsCount);
  231. }
  232. int VerticalDrumPicker::normalizedIndex(int index) const {
  233. if (!_loopData.looped) {
  234. return index;
  235. }
  236. if (index < 0) {
  237. index += _itemsCount;
  238. } else if (index >= _itemsCount) {
  239. index -= _itemsCount;
  240. }
  241. return index;
  242. }
  243. int VerticalDrumPicker::index() const {
  244. return normalizedIndex(_index + _itemsVisible.centerOffset);
  245. }
  246. rpl::producer<int> VerticalDrumPicker::changes() const {
  247. return _changes.events() | rpl::map([=] { return index(); });
  248. }
  249. rpl::producer<int> VerticalDrumPicker::value() const {
  250. return rpl::single(index())
  251. | rpl::then(changes())
  252. | rpl::distinct_until_changed();
  253. }
  254. } // namespace Ui