diff --git a/Telegram/SourceFiles/ui/widgets/vertical_drum_picker.cpp b/Telegram/SourceFiles/ui/widgets/vertical_drum_picker.cpp new file mode 100644 index 000000000..f8bee072f --- /dev/null +++ b/Telegram/SourceFiles/ui/widgets/vertical_drum_picker.cpp @@ -0,0 +1,197 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#include "ui/widgets/vertical_drum_picker.h" + +#include "ui/effects/animation_value_f.h" + +namespace Ui { + +PickerAnimation::PickerAnimation() { +} + +void PickerAnimation::jumpToOffset(int offset) { + _result.from = _result.current; + _result.to += offset; + _animation.stop(); + auto callback = [=](float64 value) { + const auto was = _result.current; + _result.current = anim::interpolateF( + _result.from, + _result.to, + value); + _updates.fire(_result.current - was); + }; + _animation.start( + std::move(callback), + 0., + 1., + st::fadeWrapDuration); +} + +void PickerAnimation::setResult(float64 from, float64 current, float64 to) { + _result = { from, current, to }; +} + +rpl::producer PickerAnimation::updates() const { + return _updates.events(); +} + +VerticalDrumPicker::VerticalDrumPicker( + not_null parent, + PaintItemCallback &&paintCallback, + int itemsCount, + int itemHeight, + int startIndex) +: RpWidget(parent) +, _itemsCount(itemsCount) +, _itemHeight(itemHeight) +, _paintCallback(std::move(paintCallback)) +, _pendingStartIndex(startIndex) { + Expects(_paintCallback != nullptr); + + sizeValue( + ) | rpl::start_with_next([=](const QSize &s) { + _itemsVisibleCount = std::ceil(float64(s.height()) / _itemHeight); + if (_pendingStartIndex && _itemsVisibleCount) { + _index = normalizedIndex(base::take(_pendingStartIndex) + - _itemsVisibleCount / 2); + } + }, lifetime()); + + paintRequest( + ) | rpl::start_with_next([=] { + Painter p(this); + + const auto outerWidth = width(); + const auto centerY = height() / 2.; + const auto shiftedY = _itemHeight * _shift; + for (auto i = -1; i < (_itemsVisibleCount + 1); i++) { + const auto y = (_itemHeight * i + shiftedY); + _paintCallback( + p, + normalizedIndex(i + _index), + y, + ((y + _itemHeight / 2.) - centerY) / centerY, + outerWidth); + } + }, lifetime()); + + _animation.updates( + ) | rpl::start_with_next([=](PickerAnimation::Shift shift) { + increaseShift(shift); + }, lifetime()); +} + +void VerticalDrumPicker::increaseShift(float64 by) { + _shift += by; + if (_shift >= 1.) { + _shift -= 1.; + _index--; + _index = normalizedIndex(_index); + } else if (_shift <= -1.) { + _shift += 1.; + _index++; + _index = normalizedIndex(_index); + } + update(); +} + +void VerticalDrumPicker::handleWheelEvent(not_null e) { + const auto direction = Ui::WheelDirection(e); + if (direction) { + _animation.jumpToOffset(direction); + } else { + increaseShift( + std::min(e->pixelDelta().y() / float64(_itemHeight), 0.99)); + if (e->phase() == Qt::ScrollEnd) { + animationDataFromIndex(); + _animation.jumpToOffset(0); + } + } +} + +void VerticalDrumPicker::handleKeyEvent(not_null e) { + if (e->key() == Qt::Key_Left || e->key() == Qt::Key_Up) { + _animation.jumpToOffset(1); + } else if (e->key() == Qt::Key_PageUp && !e->isAutoRepeat()) { + _animation.jumpToOffset(_itemsVisibleCount); + } else if (e->key() == Qt::Key_Right || e->key() == Qt::Key_Down) { + _animation.jumpToOffset(-1); + } else if (e->key() == Qt::Key_PageDown && !e->isAutoRepeat()) { + _animation.jumpToOffset(-_itemsVisibleCount); + } +} + +void VerticalDrumPicker::handleMouseEvent(not_null e) { + if (e->type() == QEvent::MouseButtonPress) { + _mouse.pressed = true; + _mouse.lastPositionY = e->pos().y(); + } else if (e->type() == QEvent::MouseMove) { + if (_mouse.pressed) { + const auto was = _mouse.lastPositionY; + _mouse.lastPositionY = e->pos().y(); + const auto diff = _mouse.lastPositionY - was; + increaseShift(float64(diff) / _itemHeight); + _mouse.clickDisabled = true; + } + } else if (e->type() == QEvent::MouseButtonRelease) { + if (_mouse.clickDisabled) { + animationDataFromIndex(); + _animation.jumpToOffset(0); + } else { + _mouse.lastPositionY = e->pos().y(); + const auto toOffset = (_itemsVisibleCount / 2) + - (_mouse.lastPositionY / _itemHeight); + _animation.jumpToOffset(toOffset); + } + _mouse = {}; + } +} + + +void VerticalDrumPicker::wheelEvent(QWheelEvent *e) { + handleWheelEvent(e); +} + +void VerticalDrumPicker::mousePressEvent(QMouseEvent *e) { + handleMouseEvent(e); +} + +void VerticalDrumPicker::mouseMoveEvent(QMouseEvent *e) { + handleMouseEvent(e); +} + +void VerticalDrumPicker::mouseReleaseEvent(QMouseEvent *e) { + handleMouseEvent(e); +} + +void VerticalDrumPicker::keyPressEvent(QKeyEvent *e) { + handleKeyEvent(e); +} + +void VerticalDrumPicker::animationDataFromIndex() { + _animation.setResult( + _index, + _index + _shift, + std::round(_index + _shift)); +} + +int VerticalDrumPicker::normalizedIndex(int index) const { + if (index < 0) { + index += _itemsCount; + } else if (index >= _itemsCount) { + index -= _itemsCount; + } + return index; +} + +int VerticalDrumPicker::index() const { + return normalizedIndex(_index + _itemsVisibleCount / 2); +} + +} // namespace Ui diff --git a/Telegram/SourceFiles/ui/widgets/vertical_drum_picker.h b/Telegram/SourceFiles/ui/widgets/vertical_drum_picker.h new file mode 100644 index 000000000..103cfb414 --- /dev/null +++ b/Telegram/SourceFiles/ui/widgets/vertical_drum_picker.h @@ -0,0 +1,91 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#pragma once + +#include "ui/rp_widget.h" + +#include "ui/effects/animations.h" + +namespace Ui { + +class PickerAnimation final { +public: + using Shift = float64; + PickerAnimation(); + + void jumpToOffset(int offset); + void setResult(float64 from, float64 current, float64 to); + + [[nodiscard]] rpl::producer updates() const; + +private: + Ui::Animations::Simple _animation; + struct { + float64 from = 0.; + float64 current = 0.; + float64 to = 0.; + } _result; + + rpl::event_stream _updates; +}; + +class VerticalDrumPicker final : public Ui::RpWidget { +public: + using PaintItemCallback = Fn; + + VerticalDrumPicker( + not_null parent, + PaintItemCallback &&paintCallback, + int itemsCount, + int itemHeight, + int startIndex = 0); + + [[nodiscard]] int index() const; + + void handleWheelEvent(not_null e); + void handleMouseEvent(not_null e); + void handleKeyEvent(not_null e); + +protected: + void wheelEvent(QWheelEvent *e) override; + void mousePressEvent(QMouseEvent *e) override; + void mouseMoveEvent(QMouseEvent *e) override; + void mouseReleaseEvent(QMouseEvent *e) override; + void keyPressEvent(QKeyEvent *e) override; + +private: + void increaseShift(float64 by); + void animationDataFromIndex(); + [[nodiscard]] int normalizedIndex(int index) const; + + const int _itemsCount; + const int _itemHeight; + + PaintItemCallback _paintCallback; + + int _pendingStartIndex = 0; + int _itemsVisibleCount = 0; + int _index = 0; + float64 _shift = 0.; + + PickerAnimation _animation; + + struct { + bool pressed = false; + int lastPositionY; + bool clickDisabled = false; + } _mouse; + +}; + +} // namespace Ui diff --git a/Telegram/cmake/td_ui.cmake b/Telegram/cmake/td_ui.cmake index eec6b9e76..d1e9fb658 100644 --- a/Telegram/cmake/td_ui.cmake +++ b/Telegram/cmake/td_ui.cmake @@ -236,6 +236,8 @@ PRIVATE ui/widgets/sent_code_field.h ui/widgets/separate_panel.cpp ui/widgets/separate_panel.h + ui/widgets/vertical_drum_picker.cpp + ui/widgets/vertical_drum_picker.h ui/cached_round_corners.cpp ui/cached_round_corners.h