Support non-zero default overscroll amounts.
This commit is contained in:
parent
081d1725af
commit
80308cea4f
2 changed files with 290 additions and 45 deletions
|
|
@ -20,6 +20,26 @@ namespace {
|
|||
|
||||
constexpr auto kOverscrollReturnDuration = crl::time(250);
|
||||
constexpr auto kOverscrollPower = 0.6;
|
||||
constexpr auto kOverscrollFromThreshold = -(1 << 30);
|
||||
constexpr auto kOverscrollTillThreshold = (1 << 30);
|
||||
|
||||
[[nodiscard]] int OverscrollFromAccumulated(int accumulated) {
|
||||
if (!accumulated) {
|
||||
return 0;
|
||||
}
|
||||
return (accumulated > 0 ? 1. : -1.)
|
||||
* int(base::SafeRound(
|
||||
pow(std::abs(accumulated), kOverscrollPower)));
|
||||
}
|
||||
|
||||
[[nodiscard]] int OverscrollToAccumulated(int overscroll) {
|
||||
if (!overscroll) {
|
||||
return 0;
|
||||
}
|
||||
return (overscroll > 0 ? 1. : -1.)
|
||||
* int(base::SafeRound(
|
||||
pow(std::abs(overscroll), 1. / kOverscrollPower)));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
|
|
@ -243,9 +263,8 @@ void ElasticScrollBar::mouseMoveEvent(QMouseEvent *e) {
|
|||
const auto position = e->globalPos();
|
||||
const auto delta = position - _dragPosition;
|
||||
_dragPosition = position;
|
||||
if (auto change = _vertical ? delta.y() : delta.x()) {
|
||||
change = scaleToBar(change);
|
||||
if (_dragOverscrollAccumulated * change < 0) {
|
||||
if (auto change = scaleToBar(_vertical ? delta.y() : delta.x())) {
|
||||
if (base::OppositeSigns(_dragOverscrollAccumulated, change)) {
|
||||
const auto overscroll = (change < 0)
|
||||
? std::max(_dragOverscrollAccumulated + change, 0)
|
||||
: std::min(_dragOverscrollAccumulated + change, 0);
|
||||
|
|
@ -264,7 +283,9 @@ void ElasticScrollBar::mouseMoveEvent(QMouseEvent *e) {
|
|||
const auto delta = now - _state.visibleFrom;
|
||||
if (change != delta) {
|
||||
_dragOverscrollAccumulated
|
||||
= ((_dragOverscrollAccumulated * change < 0)
|
||||
= (base::OppositeSigns(
|
||||
_dragOverscrollAccumulated,
|
||||
change)
|
||||
? change
|
||||
: (_dragOverscrollAccumulated + change));
|
||||
}
|
||||
|
|
@ -317,7 +338,9 @@ ElasticScroll::ElasticScroll(
|
|||
, _bar(std::make_unique<ElasticScrollBar>(this, _st, orientation))
|
||||
, _touchTimer([=] { _touchRightButton = true; })
|
||||
, _touchScrollTimer([=] { touchScrollTimer(); })
|
||||
, _vertical(orientation == Qt::Vertical) {
|
||||
, _vertical(orientation == Qt::Vertical)
|
||||
, _position(Position{ 0, 0 })
|
||||
, _movement(Movement::None) {
|
||||
setAttribute(Qt::WA_AcceptTouchEvents);
|
||||
|
||||
_bar->visibleFromDragged(
|
||||
|
|
@ -359,14 +382,6 @@ void ElasticScroll::touchDeaccelerate(int32 elapsed) {
|
|||
_touchSpeed.setY((y == 0) ? y : (y > 0) ? qMax(0, y - elapsed) : qMin(0, y + elapsed));
|
||||
}
|
||||
|
||||
int ElasticScroll::overscrollAmount() const {
|
||||
return (_state.visibleFrom < 0)
|
||||
? _state.visibleFrom
|
||||
: (_state.visibleTill > _state.fullSize)
|
||||
? (_state.visibleTill - _state.fullSize)
|
||||
: 0;
|
||||
}
|
||||
|
||||
void ElasticScroll::overscrollReturn() {
|
||||
_ignoreMomentum = _overscrollReturning = true;
|
||||
if (overscrollFinish()) {
|
||||
|
|
@ -375,6 +390,7 @@ void ElasticScroll::overscrollReturn() {
|
|||
} else if (_overscrollReturnAnimation.animating()) {
|
||||
return;
|
||||
}
|
||||
_movement = Movement::Returning;
|
||||
_overscrollReturnAnimation.start(
|
||||
[=] { applyAccumulatedScroll(); },
|
||||
0.,
|
||||
|
|
@ -383,22 +399,64 @@ void ElasticScroll::overscrollReturn() {
|
|||
anim::sineInOut);
|
||||
}
|
||||
|
||||
auto ElasticScroll::computeAccumulatedParts() const ->AccumulatedParts {
|
||||
const auto baseAccumulated = currentOverscrollDefaultAccumulated();
|
||||
const auto returnProgress = _overscrollReturnAnimation.value(
|
||||
_overscrollReturning ? 1. : 0.);
|
||||
const auto relativeAccumulated = (1. - returnProgress)
|
||||
* (_overscrollAccumulated - baseAccumulated);
|
||||
return {
|
||||
.base = baseAccumulated,
|
||||
.relative = int(base::SafeRound(relativeAccumulated)),
|
||||
};
|
||||
}
|
||||
|
||||
void ElasticScroll::overscrollReturnCancel() {
|
||||
_movement = Movement::Progress;
|
||||
if (_overscrollReturning) {
|
||||
const auto returnProgress = _overscrollReturnAnimation.value(1.);
|
||||
_overscrollAccumulated *= (1. - returnProgress);
|
||||
_overscrollReturning = false;
|
||||
const auto parts = computeAccumulatedParts();
|
||||
_overscrollAccumulated = parts.base + parts.relative;
|
||||
_overscrollReturnAnimation.stop();
|
||||
_overscrollReturning = false;
|
||||
applyAccumulatedScroll();
|
||||
}
|
||||
}
|
||||
|
||||
int ElasticScroll::currentOverscrollDefault() const {
|
||||
return (_overscroll < 0)
|
||||
? _overscrollDefaultFrom
|
||||
: (_overscroll > 0)
|
||||
? _overscrollDefaultTill
|
||||
: 0;
|
||||
}
|
||||
|
||||
int ElasticScroll::currentOverscrollDefaultAccumulated() const {
|
||||
return (_overscrollAccumulated < 0)
|
||||
? (_overscrollDefaultFrom ? kOverscrollFromThreshold : 0)
|
||||
: (_overscrollAccumulated > 0)
|
||||
? (_overscrollDefaultTill ? kOverscrollTillThreshold : 0)
|
||||
: 0;
|
||||
}
|
||||
|
||||
void ElasticScroll::overscrollCheckReturnFinish() {
|
||||
if (!_overscrollReturning) {
|
||||
return;
|
||||
} else if (!_overscrollReturnAnimation.animating()) {
|
||||
_overscrollReturning = false;
|
||||
_overscrollAccumulated = currentOverscrollDefaultAccumulated();
|
||||
_movement = Movement::None;
|
||||
} else if (overscrollFinish()) {
|
||||
_overscrollReturnAnimation.stop();
|
||||
}
|
||||
}
|
||||
|
||||
bool ElasticScroll::overscrollFinish() {
|
||||
if (overscrollAmount()) {
|
||||
if (_overscroll != currentOverscrollDefault()) {
|
||||
return false;
|
||||
}
|
||||
_overscrollReturning = false;
|
||||
_overscrollAccumulated = 0;
|
||||
_overscrollAccumulated = currentOverscrollDefaultAccumulated();
|
||||
_movement = Movement::None;
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -516,7 +574,12 @@ void ElasticScroll::paintEvent(QPaintEvent *e) {
|
|||
return;
|
||||
}
|
||||
const auto fillFrom = std::max(-_state.visibleFrom, 0);
|
||||
const auto fillTill = std::max(_state.visibleTill - _state.fullSize, 0);
|
||||
const auto content = _widget
|
||||
? (_vertical ? _widget->height() : _widget->width())
|
||||
: 0;
|
||||
const auto fillTill = content
|
||||
? std::max(_state.visibleTill - content, 0)
|
||||
: (_vertical ? height() : width());
|
||||
if (!fillFrom && !fillTill) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -561,11 +624,11 @@ bool ElasticScroll::handleWheelEvent(not_null<QWheelEvent*> e) {
|
|||
}
|
||||
}
|
||||
const auto pixels = ScrollDelta(e);
|
||||
const auto amount = overscrollAmount();
|
||||
auto delta = _vertical ? -pixels.y() : pixels.x();
|
||||
if (phase == Qt::NoScrollPhase) {
|
||||
if (!amount) {
|
||||
if (_overscroll == currentOverscrollDefault()) {
|
||||
tryScrollTo(_state.visibleFrom + delta);
|
||||
_movement = Movement::None;
|
||||
} else if (!_overscrollReturnAnimation.animating()) {
|
||||
overscrollReturn();
|
||||
}
|
||||
|
|
@ -573,10 +636,15 @@ bool ElasticScroll::handleWheelEvent(not_null<QWheelEvent*> e) {
|
|||
}
|
||||
if (!momentum) {
|
||||
overscrollReturnCancel();
|
||||
} else if (amount && !_overscrollReturnAnimation.animating()) {
|
||||
} else if (_overscroll != currentOverscrollDefault()
|
||||
&& !_overscrollReturnAnimation.animating()) {
|
||||
overscrollReturn();
|
||||
} else if (!_overscrollReturnAnimation.animating()) {
|
||||
_movement = (phase == Qt::ScrollEnd)
|
||||
? Movement::None
|
||||
: Movement::Momentum;
|
||||
}
|
||||
if (!amount) {
|
||||
if (!_overscroll) {
|
||||
const auto normalTo = willScrollTo(_state.visibleFrom + delta);
|
||||
delta -= normalTo - _state.visibleFrom;
|
||||
applyScrollTo(normalTo);
|
||||
|
|
@ -585,7 +653,13 @@ bool ElasticScroll::handleWheelEvent(not_null<QWheelEvent*> e) {
|
|||
return true;
|
||||
}
|
||||
const auto accumulated = _overscrollAccumulated + delta;
|
||||
if (_overscrollAccumulated * accumulated < 0) {
|
||||
const auto type = (accumulated < 0)
|
||||
? _overscrollTypeFrom
|
||||
: (accumulated > 0)
|
||||
? _overscrollTypeTill
|
||||
: OverscrollType::None;
|
||||
if (type == OverscrollType::None
|
||||
|| base::OppositeSigns(_overscrollAccumulated, accumulated)) {
|
||||
_overscrollAccumulated = 0;
|
||||
} else {
|
||||
_overscrollAccumulated = accumulated;
|
||||
|
|
@ -595,21 +669,15 @@ bool ElasticScroll::handleWheelEvent(not_null<QWheelEvent*> e) {
|
|||
}
|
||||
|
||||
void ElasticScroll::applyAccumulatedScroll() {
|
||||
if (_overscrollReturning) {
|
||||
if (!_overscrollReturnAnimation.animating()) {
|
||||
_overscrollReturning = false;
|
||||
_overscrollAccumulated = 0;
|
||||
} else if (overscrollFinish()) {
|
||||
_overscrollReturnAnimation.stop();
|
||||
}
|
||||
}
|
||||
const auto returnProgress = _overscrollReturnAnimation.value(
|
||||
_overscrollReturning ? 1. : 0.);
|
||||
const auto accumulated = (1. - returnProgress) * _overscrollAccumulated;
|
||||
const auto byAccumulated = (accumulated > 0 ? 1. : -1.)
|
||||
* int(base::SafeRound(pow(std::abs(accumulated), kOverscrollPower)));
|
||||
|
||||
applyScrollTo(_state.visibleFrom - overscrollAmount() + byAccumulated);
|
||||
overscrollCheckReturnFinish();
|
||||
const auto parts = computeAccumulatedParts();
|
||||
const auto baseOverscroll = (_overscrollAccumulated < 0)
|
||||
? _overscrollDefaultFrom
|
||||
: (_overscrollAccumulated > 0)
|
||||
? _overscrollDefaultTill
|
||||
: 0;
|
||||
applyOverscroll(baseOverscroll
|
||||
+ OverscrollFromAccumulated(parts.relative));
|
||||
}
|
||||
|
||||
bool ElasticScroll::eventFilter(QObject *obj, QEvent *e) {
|
||||
|
|
@ -767,7 +835,9 @@ void ElasticScroll::updateState() {
|
|||
const auto wasFullSize = _state.fullSize;
|
||||
const auto nowFullSize = _vertical ? scrollHeight() : scrollWidth();
|
||||
if (wasFullSize > nowFullSize) {
|
||||
const auto wasOverscroll = std::max(_state.visibleTill - wasFullSize, 0);
|
||||
const auto wasOverscroll = std::max(
|
||||
_state.visibleTill - wasFullSize,
|
||||
0);
|
||||
const auto nowOverscroll = std::max(till - nowFullSize, 0);
|
||||
const auto delta = std::max(
|
||||
std::min(nowOverscroll - wasOverscroll, from),
|
||||
|
|
@ -783,13 +853,32 @@ void ElasticScroll::updateState() {
|
|||
}
|
||||
|
||||
void ElasticScroll::setState(ScrollState state) {
|
||||
if (_overscroll < 0
|
||||
&& (state.visibleFrom > 0
|
||||
|| (!state.visibleFrom
|
||||
&& _overscrollTypeFrom == OverscrollType::Real))) {
|
||||
_overscroll = _overscrollDefaultFrom = 0;
|
||||
overscrollFinish();
|
||||
_overscrollReturnAnimation.stop();
|
||||
} else if (_overscroll > 0
|
||||
&& (state.visibleTill < state.fullSize
|
||||
|| (state.visibleTill == state.fullSize
|
||||
&& _overscrollTypeTill == OverscrollType::Real))) {
|
||||
_overscroll = _overscrollDefaultTill = 0;
|
||||
overscrollFinish();
|
||||
_overscrollReturnAnimation.stop();
|
||||
}
|
||||
if (_state == state) {
|
||||
_position = Position{ _state.visibleFrom, _overscroll };
|
||||
return;
|
||||
}
|
||||
const auto weak = Ui::MakeWeak(this);
|
||||
const auto old = _state.visibleFrom;
|
||||
_state = state;
|
||||
_bar->updateState(state);
|
||||
if (weak) {
|
||||
_position = Position{ _state.visibleFrom, _overscroll };
|
||||
}
|
||||
if (weak && _state.visibleFrom != old) {
|
||||
if (_vertical) {
|
||||
_scrollTopUpdated.fire_copy(_state.visibleFrom);
|
||||
|
|
@ -819,6 +908,27 @@ void ElasticScroll::applyScrollTo(int position, bool synthMouseMove) {
|
|||
}
|
||||
}
|
||||
|
||||
void ElasticScroll::applyOverscroll(int overscroll) {
|
||||
if (_overscroll == overscroll) {
|
||||
return;
|
||||
}
|
||||
_overscroll = overscroll;
|
||||
const auto max = _state.fullSize
|
||||
- (_state.visibleTill - _state.visibleFrom);
|
||||
if (_overscroll > 0) {
|
||||
const auto added = (_overscrollTypeTill == OverscrollType::Real)
|
||||
? _overscroll
|
||||
: 0;
|
||||
applyScrollTo(max + added);
|
||||
} else if (_overscroll < 0) {
|
||||
applyScrollTo((_overscrollTypeFrom == OverscrollType::Real)
|
||||
? _overscroll
|
||||
: 0);
|
||||
} else {
|
||||
applyScrollTo(std::clamp(_state.visibleFrom, 0, max));
|
||||
}
|
||||
}
|
||||
|
||||
int ElasticScroll::willScrollTo(int position) const {
|
||||
return std::clamp(
|
||||
position,
|
||||
|
|
@ -968,6 +1078,78 @@ void ElasticScroll::updateBars() {
|
|||
_bar->update();
|
||||
}
|
||||
|
||||
void ElasticScroll::setOverscrollTypes(
|
||||
OverscrollType from,
|
||||
OverscrollType till) {
|
||||
const auto fromChanged = (_overscroll < 0)
|
||||
&& (_overscrollTypeFrom != from);
|
||||
const auto tillChanged = (_overscroll > 0)
|
||||
&& (_overscrollTypeTill != till);
|
||||
_overscrollTypeFrom = from;
|
||||
_overscrollTypeTill = till;
|
||||
if (fromChanged) {
|
||||
switch (_overscrollTypeFrom) {
|
||||
case OverscrollType::None:
|
||||
_overscroll = 0;
|
||||
applyScrollTo(0);
|
||||
break;
|
||||
case OverscrollType::Virtual:
|
||||
applyScrollTo(0);
|
||||
break;
|
||||
case OverscrollType::Real:
|
||||
applyScrollTo(_overscroll);
|
||||
break;
|
||||
}
|
||||
} else if (tillChanged) {
|
||||
const auto max = _state.fullSize
|
||||
- (_state.visibleTill - _state.visibleFrom);
|
||||
switch (_overscrollTypeTill) {
|
||||
case OverscrollType::None:
|
||||
_overscroll = 0;
|
||||
applyScrollTo(max);
|
||||
break;
|
||||
case OverscrollType::Virtual:
|
||||
applyScrollTo(max);
|
||||
break;
|
||||
case OverscrollType::Real:
|
||||
applyScrollTo(max + _overscroll);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ElasticScroll::setOverscrollDefaults(int from, int till) {
|
||||
Expects(from <= 0 && till >= 0);
|
||||
|
||||
const auto fromChanged = (_overscrollDefaultFrom != from);
|
||||
const auto tillChanged = (_overscrollDefaultTill != till);
|
||||
const auto changed = (fromChanged && _overscroll < 0)
|
||||
|| (tillChanged && _overscroll > 0);
|
||||
const auto movement = _movement.current();
|
||||
if (_overscrollReturnAnimation.animating()) {
|
||||
overscrollReturnCancel();
|
||||
}
|
||||
_overscrollDefaultFrom = from;
|
||||
_overscrollDefaultTill = till;
|
||||
if (changed) {
|
||||
const auto delta = (_overscroll < 0)
|
||||
? (_overscroll - _overscrollDefaultFrom)
|
||||
: (_overscroll - _overscrollDefaultTill);
|
||||
_overscrollAccumulated = currentOverscrollDefaultAccumulated()
|
||||
+ OverscrollToAccumulated(delta);
|
||||
}
|
||||
if (movement == Movement::Momentum || movement == Movement::Returning) {
|
||||
if (_overscroll != currentOverscrollDefault()) {
|
||||
overscrollReturn();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ElasticScroll::setOverscrollBg(QColor bg) {
|
||||
_overscrollBg = bg;
|
||||
update();
|
||||
}
|
||||
|
||||
rpl::producer<> ElasticScroll::scrolls() const {
|
||||
return _scrolls.events();
|
||||
}
|
||||
|
|
@ -980,6 +1162,22 @@ rpl::producer<> ElasticScroll::geometryChanged() const {
|
|||
return _geometryChanged.events();
|
||||
}
|
||||
|
||||
ElasticScrollPosition ElasticScroll::position() const {
|
||||
return _position.current();
|
||||
}
|
||||
|
||||
rpl::producer<ElasticScrollPosition> ElasticScroll::positionValue() const {
|
||||
return _position.value();
|
||||
}
|
||||
|
||||
ElasticScrollMovement ElasticScroll::movement() const {
|
||||
return _movement.current();
|
||||
}
|
||||
|
||||
rpl::producer<ElasticScrollMovement> ElasticScroll::movementValue() const {
|
||||
return _movement.value();
|
||||
}
|
||||
|
||||
QPoint ScrollDelta(not_null<QWheelEvent*> e) {
|
||||
const auto convert = [](QPoint point) {
|
||||
return QPoint(
|
||||
|
|
|
|||
|
|
@ -85,6 +85,25 @@ private:
|
|||
|
||||
};
|
||||
|
||||
struct ElasticScrollPosition {
|
||||
int value = 0;
|
||||
int overscroll = 0;
|
||||
|
||||
friend inline auto operator<=>(
|
||||
ElasticScrollPosition,
|
||||
ElasticScrollPosition) = default;
|
||||
friend inline bool operator==(
|
||||
ElasticScrollPosition,
|
||||
ElasticScrollPosition) = default;
|
||||
};
|
||||
|
||||
enum class ElasticScrollMovement {
|
||||
None,
|
||||
Progress,
|
||||
Momentum,
|
||||
Returning,
|
||||
};
|
||||
|
||||
class ElasticScroll final : public RpWidget {
|
||||
public:
|
||||
ElasticScroll(
|
||||
|
|
@ -136,15 +155,28 @@ public:
|
|||
void setCustomTouchProcess(Fn<bool(not_null<QTouchEvent*>)> process) {
|
||||
_customTouchProcess = std::move(process);
|
||||
}
|
||||
void setOverscrollBg(QColor bg) {
|
||||
_overscrollBg = bg;
|
||||
update();
|
||||
}
|
||||
|
||||
enum class OverscrollType : uchar {
|
||||
None,
|
||||
Virtual,
|
||||
Real,
|
||||
};
|
||||
void setOverscrollTypes(OverscrollType from, OverscrollType till);
|
||||
void setOverscrollDefaults(int from, int till);
|
||||
void setOverscrollBg(QColor bg);
|
||||
|
||||
[[nodiscard]] rpl::producer<> scrolls() const;
|
||||
[[nodiscard]] rpl::producer<> innerResizes() const;
|
||||
[[nodiscard]] rpl::producer<> geometryChanged() const;
|
||||
|
||||
using Position = ElasticScrollPosition;
|
||||
[[nodiscard]] Position position() const;
|
||||
[[nodiscard]] rpl::producer<Position> positionValue() const;
|
||||
|
||||
using Movement = ElasticScrollMovement;
|
||||
[[nodiscard]] Movement movement() const;
|
||||
[[nodiscard]] rpl::producer<Movement> movementValue() const;
|
||||
|
||||
private:
|
||||
bool eventHook(QEvent *e) override;
|
||||
bool eventFilter(QObject *obj, QEvent *e) override;
|
||||
|
|
@ -163,6 +195,7 @@ private:
|
|||
[[nodiscard]] int willScrollTo(int position) const;
|
||||
void tryScrollTo(int position, bool synthMouseMove = true);
|
||||
void applyScrollTo(int position, bool synthMouseMove = true);
|
||||
void applyOverscroll(int overscroll);
|
||||
|
||||
void doSetOwnedWidget(object_ptr<QWidget> widget);
|
||||
object_ptr<QWidget> doTakeWidget();
|
||||
|
|
@ -176,9 +209,16 @@ private:
|
|||
void touchUpdateSpeed();
|
||||
void touchDeaccelerate(int32 elapsed);
|
||||
|
||||
[[nodiscard]] int overscrollAmount() const;
|
||||
struct AccumulatedParts {
|
||||
int base = 0;
|
||||
int relative = 0;
|
||||
};
|
||||
[[nodiscard]] AccumulatedParts computeAccumulatedParts() const;
|
||||
[[nodiscard]] int currentOverscrollDefault() const;
|
||||
[[nodiscard]] int currentOverscrollDefaultAccumulated() const;
|
||||
void overscrollReturn();
|
||||
void overscrollReturnCancel();
|
||||
void overscrollCheckReturnFinish();
|
||||
bool overscrollFinish();
|
||||
void applyAccumulatedScroll();
|
||||
|
||||
|
|
@ -213,8 +253,15 @@ private:
|
|||
|
||||
Fn<bool(not_null<QWheelEvent*>)> _customWheelProcess;
|
||||
Fn<bool(not_null<QTouchEvent*>)> _customTouchProcess;
|
||||
int _overscroll = 0;
|
||||
int _overscrollDefaultFrom = 0;
|
||||
int _overscrollDefaultTill = 0;
|
||||
OverscrollType _overscrollTypeFrom = OverscrollType::None;
|
||||
OverscrollType _overscrollTypeTill = OverscrollType::None;
|
||||
std::optional<QColor> _overscrollBg;
|
||||
Ui::Animations::Simple _overscrollReturnAnimation;
|
||||
rpl::variable<Position> _position;
|
||||
rpl::variable<Movement> _movement;
|
||||
|
||||
object_ptr<QWidget> _widget = { nullptr };
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue