Support non-zero default overscroll amounts.

This commit is contained in:
John Preston 2023-07-11 11:13:17 +04:00
parent 081d1725af
commit 80308cea4f
2 changed files with 290 additions and 45 deletions

View file

@ -20,6 +20,26 @@ namespace {
constexpr auto kOverscrollReturnDuration = crl::time(250); constexpr auto kOverscrollReturnDuration = crl::time(250);
constexpr auto kOverscrollPower = 0.6; 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 } // namespace
@ -243,9 +263,8 @@ void ElasticScrollBar::mouseMoveEvent(QMouseEvent *e) {
const auto position = e->globalPos(); const auto position = e->globalPos();
const auto delta = position - _dragPosition; const auto delta = position - _dragPosition;
_dragPosition = position; _dragPosition = position;
if (auto change = _vertical ? delta.y() : delta.x()) { if (auto change = scaleToBar(_vertical ? delta.y() : delta.x())) {
change = scaleToBar(change); if (base::OppositeSigns(_dragOverscrollAccumulated, change)) {
if (_dragOverscrollAccumulated * change < 0) {
const auto overscroll = (change < 0) const auto overscroll = (change < 0)
? std::max(_dragOverscrollAccumulated + change, 0) ? std::max(_dragOverscrollAccumulated + change, 0)
: std::min(_dragOverscrollAccumulated + change, 0); : std::min(_dragOverscrollAccumulated + change, 0);
@ -264,7 +283,9 @@ void ElasticScrollBar::mouseMoveEvent(QMouseEvent *e) {
const auto delta = now - _state.visibleFrom; const auto delta = now - _state.visibleFrom;
if (change != delta) { if (change != delta) {
_dragOverscrollAccumulated _dragOverscrollAccumulated
= ((_dragOverscrollAccumulated * change < 0) = (base::OppositeSigns(
_dragOverscrollAccumulated,
change)
? change ? change
: (_dragOverscrollAccumulated + change)); : (_dragOverscrollAccumulated + change));
} }
@ -317,7 +338,9 @@ ElasticScroll::ElasticScroll(
, _bar(std::make_unique<ElasticScrollBar>(this, _st, orientation)) , _bar(std::make_unique<ElasticScrollBar>(this, _st, orientation))
, _touchTimer([=] { _touchRightButton = true; }) , _touchTimer([=] { _touchRightButton = true; })
, _touchScrollTimer([=] { touchScrollTimer(); }) , _touchScrollTimer([=] { touchScrollTimer(); })
, _vertical(orientation == Qt::Vertical) { , _vertical(orientation == Qt::Vertical)
, _position(Position{ 0, 0 })
, _movement(Movement::None) {
setAttribute(Qt::WA_AcceptTouchEvents); setAttribute(Qt::WA_AcceptTouchEvents);
_bar->visibleFromDragged( _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)); _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() { void ElasticScroll::overscrollReturn() {
_ignoreMomentum = _overscrollReturning = true; _ignoreMomentum = _overscrollReturning = true;
if (overscrollFinish()) { if (overscrollFinish()) {
@ -375,6 +390,7 @@ void ElasticScroll::overscrollReturn() {
} else if (_overscrollReturnAnimation.animating()) { } else if (_overscrollReturnAnimation.animating()) {
return; return;
} }
_movement = Movement::Returning;
_overscrollReturnAnimation.start( _overscrollReturnAnimation.start(
[=] { applyAccumulatedScroll(); }, [=] { applyAccumulatedScroll(); },
0., 0.,
@ -383,22 +399,64 @@ void ElasticScroll::overscrollReturn() {
anim::sineInOut); 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() { void ElasticScroll::overscrollReturnCancel() {
_movement = Movement::Progress;
if (_overscrollReturning) { if (_overscrollReturning) {
const auto returnProgress = _overscrollReturnAnimation.value(1.); const auto parts = computeAccumulatedParts();
_overscrollAccumulated *= (1. - returnProgress); _overscrollAccumulated = parts.base + parts.relative;
_overscrollReturning = false;
_overscrollReturnAnimation.stop(); _overscrollReturnAnimation.stop();
_overscrollReturning = false;
applyAccumulatedScroll(); 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() { bool ElasticScroll::overscrollFinish() {
if (overscrollAmount()) { if (_overscroll != currentOverscrollDefault()) {
return false; return false;
} }
_overscrollReturning = false; _overscrollReturning = false;
_overscrollAccumulated = 0; _overscrollAccumulated = currentOverscrollDefaultAccumulated();
_movement = Movement::None;
return true; return true;
} }
@ -516,7 +574,12 @@ void ElasticScroll::paintEvent(QPaintEvent *e) {
return; return;
} }
const auto fillFrom = std::max(-_state.visibleFrom, 0); 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) { if (!fillFrom && !fillTill) {
return; return;
} }
@ -561,11 +624,11 @@ bool ElasticScroll::handleWheelEvent(not_null<QWheelEvent*> e) {
} }
} }
const auto pixels = ScrollDelta(e); const auto pixels = ScrollDelta(e);
const auto amount = overscrollAmount();
auto delta = _vertical ? -pixels.y() : pixels.x(); auto delta = _vertical ? -pixels.y() : pixels.x();
if (phase == Qt::NoScrollPhase) { if (phase == Qt::NoScrollPhase) {
if (!amount) { if (_overscroll == currentOverscrollDefault()) {
tryScrollTo(_state.visibleFrom + delta); tryScrollTo(_state.visibleFrom + delta);
_movement = Movement::None;
} else if (!_overscrollReturnAnimation.animating()) { } else if (!_overscrollReturnAnimation.animating()) {
overscrollReturn(); overscrollReturn();
} }
@ -573,10 +636,15 @@ bool ElasticScroll::handleWheelEvent(not_null<QWheelEvent*> e) {
} }
if (!momentum) { if (!momentum) {
overscrollReturnCancel(); overscrollReturnCancel();
} else if (amount && !_overscrollReturnAnimation.animating()) { } else if (_overscroll != currentOverscrollDefault()
&& !_overscrollReturnAnimation.animating()) {
overscrollReturn(); overscrollReturn();
} else if (!_overscrollReturnAnimation.animating()) {
_movement = (phase == Qt::ScrollEnd)
? Movement::None
: Movement::Momentum;
} }
if (!amount) { if (!_overscroll) {
const auto normalTo = willScrollTo(_state.visibleFrom + delta); const auto normalTo = willScrollTo(_state.visibleFrom + delta);
delta -= normalTo - _state.visibleFrom; delta -= normalTo - _state.visibleFrom;
applyScrollTo(normalTo); applyScrollTo(normalTo);
@ -585,7 +653,13 @@ bool ElasticScroll::handleWheelEvent(not_null<QWheelEvent*> e) {
return true; return true;
} }
const auto accumulated = _overscrollAccumulated + delta; 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; _overscrollAccumulated = 0;
} else { } else {
_overscrollAccumulated = accumulated; _overscrollAccumulated = accumulated;
@ -595,21 +669,15 @@ bool ElasticScroll::handleWheelEvent(not_null<QWheelEvent*> e) {
} }
void ElasticScroll::applyAccumulatedScroll() { void ElasticScroll::applyAccumulatedScroll() {
if (_overscrollReturning) { overscrollCheckReturnFinish();
if (!_overscrollReturnAnimation.animating()) { const auto parts = computeAccumulatedParts();
_overscrollReturning = false; const auto baseOverscroll = (_overscrollAccumulated < 0)
_overscrollAccumulated = 0; ? _overscrollDefaultFrom
} else if (overscrollFinish()) { : (_overscrollAccumulated > 0)
_overscrollReturnAnimation.stop(); ? _overscrollDefaultTill
} : 0;
} applyOverscroll(baseOverscroll
const auto returnProgress = _overscrollReturnAnimation.value( + OverscrollFromAccumulated(parts.relative));
_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);
} }
bool ElasticScroll::eventFilter(QObject *obj, QEvent *e) { bool ElasticScroll::eventFilter(QObject *obj, QEvent *e) {
@ -767,7 +835,9 @@ void ElasticScroll::updateState() {
const auto wasFullSize = _state.fullSize; const auto wasFullSize = _state.fullSize;
const auto nowFullSize = _vertical ? scrollHeight() : scrollWidth(); const auto nowFullSize = _vertical ? scrollHeight() : scrollWidth();
if (wasFullSize > nowFullSize) { 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 nowOverscroll = std::max(till - nowFullSize, 0);
const auto delta = std::max( const auto delta = std::max(
std::min(nowOverscroll - wasOverscroll, from), std::min(nowOverscroll - wasOverscroll, from),
@ -783,13 +853,32 @@ void ElasticScroll::updateState() {
} }
void ElasticScroll::setState(ScrollState state) { 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) { if (_state == state) {
_position = Position{ _state.visibleFrom, _overscroll };
return; return;
} }
const auto weak = Ui::MakeWeak(this); const auto weak = Ui::MakeWeak(this);
const auto old = _state.visibleFrom; const auto old = _state.visibleFrom;
_state = state; _state = state;
_bar->updateState(state); _bar->updateState(state);
if (weak) {
_position = Position{ _state.visibleFrom, _overscroll };
}
if (weak && _state.visibleFrom != old) { if (weak && _state.visibleFrom != old) {
if (_vertical) { if (_vertical) {
_scrollTopUpdated.fire_copy(_state.visibleFrom); _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 { int ElasticScroll::willScrollTo(int position) const {
return std::clamp( return std::clamp(
position, position,
@ -968,6 +1078,78 @@ void ElasticScroll::updateBars() {
_bar->update(); _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 { rpl::producer<> ElasticScroll::scrolls() const {
return _scrolls.events(); return _scrolls.events();
} }
@ -980,6 +1162,22 @@ rpl::producer<> ElasticScroll::geometryChanged() const {
return _geometryChanged.events(); 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) { QPoint ScrollDelta(not_null<QWheelEvent*> e) {
const auto convert = [](QPoint point) { const auto convert = [](QPoint point) {
return QPoint( return QPoint(

View file

@ -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 { class ElasticScroll final : public RpWidget {
public: public:
ElasticScroll( ElasticScroll(
@ -136,15 +155,28 @@ public:
void setCustomTouchProcess(Fn<bool(not_null<QTouchEvent*>)> process) { void setCustomTouchProcess(Fn<bool(not_null<QTouchEvent*>)> process) {
_customTouchProcess = std::move(process); _customTouchProcess = std::move(process);
} }
void setOverscrollBg(QColor bg) {
_overscrollBg = bg; enum class OverscrollType : uchar {
update(); 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<> scrolls() const;
[[nodiscard]] rpl::producer<> innerResizes() const; [[nodiscard]] rpl::producer<> innerResizes() const;
[[nodiscard]] rpl::producer<> geometryChanged() 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: private:
bool eventHook(QEvent *e) override; bool eventHook(QEvent *e) override;
bool eventFilter(QObject *obj, QEvent *e) override; bool eventFilter(QObject *obj, QEvent *e) override;
@ -163,6 +195,7 @@ private:
[[nodiscard]] int willScrollTo(int position) const; [[nodiscard]] int willScrollTo(int position) const;
void tryScrollTo(int position, bool synthMouseMove = true); void tryScrollTo(int position, bool synthMouseMove = true);
void applyScrollTo(int position, bool synthMouseMove = true); void applyScrollTo(int position, bool synthMouseMove = true);
void applyOverscroll(int overscroll);
void doSetOwnedWidget(object_ptr<QWidget> widget); void doSetOwnedWidget(object_ptr<QWidget> widget);
object_ptr<QWidget> doTakeWidget(); object_ptr<QWidget> doTakeWidget();
@ -176,9 +209,16 @@ private:
void touchUpdateSpeed(); void touchUpdateSpeed();
void touchDeaccelerate(int32 elapsed); 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 overscrollReturn();
void overscrollReturnCancel(); void overscrollReturnCancel();
void overscrollCheckReturnFinish();
bool overscrollFinish(); bool overscrollFinish();
void applyAccumulatedScroll(); void applyAccumulatedScroll();
@ -213,8 +253,15 @@ private:
Fn<bool(not_null<QWheelEvent*>)> _customWheelProcess; Fn<bool(not_null<QWheelEvent*>)> _customWheelProcess;
Fn<bool(not_null<QTouchEvent*>)> _customTouchProcess; 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; std::optional<QColor> _overscrollBg;
Ui::Animations::Simple _overscrollReturnAnimation; Ui::Animations::Simple _overscrollReturnAnimation;
rpl::variable<Position> _position;
rpl::variable<Movement> _movement;
object_ptr<QWidget> _widget = { nullptr }; object_ptr<QWidget> _widget = { nullptr };