1286 lines
		
	
	
	
		
			35 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
			
		
		
	
	
			1286 lines
		
	
	
	
		
			35 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
| // This file is part of Desktop App Toolkit,
 | |
| // a set of libraries for developing nice desktop applications.
 | |
| //
 | |
| // For license and copyright information please follow this link:
 | |
| // https://github.com/desktop-app/legal/blob/master/LEGAL
 | |
| //
 | |
| #include "ui/widgets/elastic_scroll.h"
 | |
| 
 | |
| #include "ui/painter.h"
 | |
| #include "ui/ui_utility.h"
 | |
| #include "base/platform/base_platform_info.h"
 | |
| #include "base/qt/qt_common_adapters.h"
 | |
| #include "styles/style_widgets.h"
 | |
| 
 | |
| #include <QtGui/QWindow>
 | |
| #include <QtCore/QtMath>
 | |
| #include <QtWidgets/QApplication>
 | |
| 
 | |
| namespace Ui {
 | |
| namespace {
 | |
| 
 | |
| constexpr auto kOverscrollReturnDuration = crl::time(250);
 | |
| //constexpr auto kOverscrollPower = 0.6;
 | |
| constexpr auto kOverscrollFromThreshold = -(1 << 30);
 | |
| constexpr auto kOverscrollTillThreshold = (1 << 30);
 | |
| constexpr auto kTouchOverscrollMultiplier = 2;
 | |
| 
 | |
| constexpr auto kLogA = 16.;
 | |
| constexpr auto kLogB = 10.;
 | |
| 
 | |
| [[nodiscard]] float64 RawFrom(float64 value) {
 | |
| 	const auto scale = style::Scale() / 100.;
 | |
| 	value /= scale;
 | |
| 
 | |
| 	const auto result = kLogA * log(1. + value / kLogB);
 | |
| 	//const auto result = pow(value, kOverscrollPower);
 | |
| 
 | |
| 	return result * scale;
 | |
| }
 | |
| 
 | |
| [[nodiscard]] float64 RawTo(float64 value) {
 | |
| 	const auto scale = style::Scale() / 100.;
 | |
| 	value /= scale;
 | |
| 
 | |
| 	const auto result = (exp(value / kLogA) - 1.) * kLogB;
 | |
| 	//const auto result = pow(value, 1. / kOverscrollPower);
 | |
| 
 | |
| 	return result * scale;
 | |
| }
 | |
| 
 | |
| [[nodiscard]] int OverscrollFromAccumulated(int accumulated) {
 | |
| 	if (!accumulated) {
 | |
| 		return 0;
 | |
| 	}
 | |
| 
 | |
| 	return (accumulated > 0 ? 1. : -1.)
 | |
| 		* int(base::SafeRound(RawFrom(std::abs(accumulated))));
 | |
| }
 | |
| 
 | |
| [[nodiscard]] int OverscrollToAccumulated(int overscroll) {
 | |
| 	if (!overscroll) {
 | |
| 		return 0;
 | |
| 	}
 | |
| 	return (overscroll > 0 ? 1. : -1.)
 | |
| 		* int(base::SafeRound(RawTo(std::abs(overscroll))));
 | |
| }
 | |
| 
 | |
| } // namespace
 | |
| 
 | |
| // Flick scroll taken from
 | |
| // http://qt-project.org/doc/qt-4.8
 | |
| // /demos-embedded-anomaly-src-flickcharm-cpp.html
 | |
| 
 | |
| ElasticScrollBar::ElasticScrollBar(
 | |
| 	QWidget *parent,
 | |
| 	const style::ScrollArea &st,
 | |
| 	Qt::Orientation orientation)
 | |
| : RpWidget(parent)
 | |
| , _st(st)
 | |
| , _hideTimer([=] { toggle(false); })
 | |
| , _shown(!_st.hiding)
 | |
| , _vertical(orientation == Qt::Vertical) {
 | |
| 	setAttribute(Qt::WA_NoMousePropagation);
 | |
| }
 | |
| 
 | |
| void ElasticScrollBar::refreshGeometry() {
 | |
| 	update();
 | |
| 	const auto skip = _st.deltax;
 | |
| 	const auto fullSkip = _st.deltat + _st.deltab;
 | |
| 	const auto extSize = _vertical ? height() : width();
 | |
| 	const auto thickness = (_vertical ? width() : height()) - 2 * skip;
 | |
| 	const auto minSize = fullSkip + 2 * thickness;
 | |
| 	if (_state.fullSize <= 0
 | |
| 		|| _state.visibleFrom >= _state.visibleTill
 | |
| 		|| extSize < minSize) {
 | |
| 		_bar = _area = QRect();
 | |
| 		hide();
 | |
| 		return;
 | |
| 	}
 | |
| 	const auto available = extSize - fullSkip;
 | |
| 	_area = _vertical
 | |
| 		? QRect(skip, _st.deltat, thickness, available)
 | |
| 		: QRect(_st.deltat, skip, available, thickness);
 | |
| 	const auto barMin = std::min(st::scrollBarMin, available / 2);
 | |
| 	const auto visibleHeight = _state.visibleTill - _state.visibleFrom;
 | |
| 	const auto scrollableHeight = _state.fullSize - visibleHeight;
 | |
| 	const auto barWanted = (available * visibleHeight) / _state.fullSize;
 | |
| 	if (barWanted >= available) {
 | |
| 		_bar = _area = QRect();
 | |
| 		hide();
 | |
| 		return;
 | |
| 	}
 | |
| 	const auto bar = std::max(barMin, barWanted);
 | |
| 	const auto outsideBar = available - bar;
 | |
| 
 | |
| 	const auto scale = [&](int value) {
 | |
| 		return (outsideBar * value) / scrollableHeight;
 | |
| 	};
 | |
| 	const auto barFrom = scale(_state.visibleFrom);
 | |
| 	const auto barTill = barFrom + bar;
 | |
| 	const auto cutFrom = std::clamp(barFrom, 0, available - thickness);
 | |
| 	const auto cutTill = std::clamp(barTill, thickness, available);
 | |
| 	const auto cutBar = cutTill - cutFrom;
 | |
| 	_bar = _vertical
 | |
| 		? QRect(_area.x(), _area.y() + cutFrom, _area.width(), cutBar)
 | |
| 		: QRect(_area.x() + cutFrom, _area.y(), cutBar, _area.height());
 | |
| 	if (isHidden()) {
 | |
| 		show();
 | |
| 	}
 | |
| }
 | |
| 
 | |
| bool ElasticScrollBar::barHighlighted() const {
 | |
| 	return _overBar || _dragging;
 | |
| }
 | |
| 
 | |
| void ElasticScrollBar::toggle(bool shown, anim::type animated) {
 | |
| 	const auto instant = (animated == anim::type::instant);
 | |
| 	const auto changed = (_shown != shown);
 | |
| 	_shown = shown;
 | |
| 	if (instant) {
 | |
| 		_shownAnimation.stop();
 | |
| 	}
 | |
| 	if (_shown && _st.hiding) {
 | |
| 		_hideTimer.callOnce(_st.hiding);
 | |
| 	}
 | |
| 	if (changed && !instant) {
 | |
| 		_shownAnimation.start(
 | |
| 			[=] { update(); },
 | |
| 			_shown ? 0. : 1.,
 | |
| 			_shown ? 1. : 0.,
 | |
| 			_st.duration);
 | |
| 	}
 | |
| 	update();
 | |
| }
 | |
| 
 | |
| void ElasticScrollBar::toggleOver(bool over, anim::type animated) {
 | |
| 	const auto instant = (animated == anim::type::instant);
 | |
| 	const auto changed = (_over != over);
 | |
| 	_over = over;
 | |
| 	if (instant) {
 | |
| 		_overAnimation.stop();
 | |
| 	}
 | |
| 	if (!instant && changed) {
 | |
| 		_overAnimation.start(
 | |
| 			[=] { update(); },
 | |
| 			_over ? 0. : 1.,
 | |
| 			_over ? 1. : 0.,
 | |
| 			_st.duration);
 | |
| 	}
 | |
| 	update();
 | |
| }
 | |
| 
 | |
| void ElasticScrollBar::toggleOverBar(bool over, anim::type animated) {
 | |
| 	const auto instant = (animated == anim::type::instant);
 | |
| 	const auto wasHighlight = barHighlighted();
 | |
| 	_overBar = over;
 | |
| 	if (instant) {
 | |
| 		_barHighlightAnimation.stop();
 | |
| 	} else {
 | |
| 		startBarHighlightAnimation(wasHighlight);
 | |
| 	}
 | |
| 	update();
 | |
| }
 | |
| 
 | |
| void ElasticScrollBar::toggleDragging(bool dragging, anim::type animated) {
 | |
| 	const auto instant = (animated == anim::type::instant);
 | |
| 	const auto wasHighlight = barHighlighted();
 | |
| 	_dragging = dragging;
 | |
| 	if (instant) {
 | |
| 		_barHighlightAnimation.stop();
 | |
| 	} else {
 | |
| 		startBarHighlightAnimation(wasHighlight);
 | |
| 	}
 | |
| 	update();
 | |
| }
 | |
| 
 | |
| void ElasticScrollBar::startBarHighlightAnimation(bool wasHighlighted) {
 | |
| 	if (barHighlighted() == wasHighlighted) {
 | |
| 		return;
 | |
| 	}
 | |
| 	const auto highlighted = !wasHighlighted;
 | |
| 	_barHighlightAnimation.start(
 | |
| 		[=] { update(); },
 | |
| 		highlighted ? 0. : 1.,
 | |
| 		highlighted ? 1. : 0.,
 | |
| 		_st.duration);
 | |
| }
 | |
| 
 | |
| rpl::producer<int> ElasticScrollBar::visibleFromDragged() const {
 | |
| 	return _visibleFromDragged.events();
 | |
| }
 | |
| 
 | |
| void ElasticScrollBar::updateState(ScrollState state) {
 | |
| 	if (_state != state) {
 | |
| 		_state = state;
 | |
| 		refreshGeometry();
 | |
| 		toggle(true);
 | |
| 	}
 | |
| }
 | |
| 
 | |
| void ElasticScrollBar::paintEvent(QPaintEvent *e) {
 | |
| 	if (_bar.isEmpty()) {
 | |
| 		hide();
 | |
| 		return;
 | |
| 	}
 | |
| 	const auto barHighlight = _barHighlightAnimation.value(
 | |
| 		barHighlighted() ? 1. : 0.);
 | |
| 	const auto over = std::max(
 | |
| 		_overAnimation.value(_over ? 1. : 0.),
 | |
| 		barHighlight);
 | |
| 	const auto shown = std::max(
 | |
| 		_shownAnimation.value(_shown ? 1. : 0.),
 | |
| 		over);
 | |
| 	if (shown < 1. / 255) {
 | |
| 		return;
 | |
| 	}
 | |
| 	QPainter p(this);
 | |
| 	p.setPen(Qt::NoPen);
 | |
| 	auto bg = anim::color(_st.bg, _st.bgOver, over);
 | |
| 	bg.setAlpha(anim::interpolate(0, bg.alpha(), shown));
 | |
| 	auto bar = anim::color(_st.barBg, _st.barBgOver, barHighlight);
 | |
| 	bar.setAlpha(anim::interpolate(0, bar.alpha(), shown));
 | |
| 	const auto radius = (_st.round < 0)
 | |
| 		? (std::min(_area.width(), _area.height()) / 2.)
 | |
| 		: _st.round;
 | |
| 	if (radius) {
 | |
| 		PainterHighQualityEnabler hq(p);
 | |
| 		p.setBrush(bg);
 | |
| 		p.drawRoundedRect(_area, radius, radius);
 | |
| 		p.setBrush(bar);
 | |
| 		p.drawRoundedRect(_bar, radius, radius);
 | |
| 	} else {
 | |
| 		p.fillRect(_area, bg);
 | |
| 		p.fillRect(_bar, bar);
 | |
| 	}
 | |
| }
 | |
| 
 | |
| void ElasticScrollBar::enterEventHook(QEnterEvent *e) {
 | |
| 	_hideTimer.cancel();
 | |
| 	setMouseTracking(true);
 | |
| 	toggleOver(true);
 | |
| }
 | |
| 
 | |
| void ElasticScrollBar::leaveEventHook(QEvent *e) {
 | |
| 	if (!_dragging) {
 | |
| 		setMouseTracking(false);
 | |
| 	}
 | |
| 	toggleOver(false);
 | |
| 	toggleOverBar(false);
 | |
| 	if (_st.hiding && _shown) {
 | |
| 		_hideTimer.callOnce(_st.hiding);
 | |
| 	}
 | |
| }
 | |
| 
 | |
| int ElasticScrollBar::scaleToBar(int change) const {
 | |
| 	const auto scrollable = _state.fullSize
 | |
| 		- (_state.visibleTill - _state.visibleFrom);
 | |
| 	const auto outsideBar = (_vertical ? _area.height() : _area.width())
 | |
| 		- (_vertical ? _bar.height() : _bar.width());
 | |
| 	return (outsideBar <= 0 || scrollable <= outsideBar)
 | |
| 		? change
 | |
| 		: (change * scrollable / outsideBar);
 | |
| }
 | |
| 
 | |
| void ElasticScrollBar::mouseMoveEvent(QMouseEvent *e) {
 | |
| 	toggleOverBar(_bar.contains(e->pos()));
 | |
| 	if (_dragging && !_bar.isEmpty()) {
 | |
| 		const auto position = e->globalPos();
 | |
| 		const auto delta = position - _dragPosition;
 | |
| 		_dragPosition = position;
 | |
| 		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);
 | |
| 				const auto delta = overscroll - _dragOverscrollAccumulated;
 | |
| 				_dragOverscrollAccumulated = overscroll;
 | |
| 				change -= delta;
 | |
| 			}
 | |
| 			if (change) {
 | |
| 				const auto now = std::clamp(
 | |
| 					_state.visibleFrom + change,
 | |
| 					std::min(_state.visibleFrom, 0),
 | |
| 					std::max(
 | |
| 						_state.visibleFrom,
 | |
| 						(_state.visibleFrom
 | |
| 							+ (_state.fullSize - _state.visibleTill))));
 | |
| 				const auto delta = now - _state.visibleFrom;
 | |
| 				if (change != delta) {
 | |
| 					_dragOverscrollAccumulated
 | |
| 						= (base::OppositeSigns(
 | |
| 							_dragOverscrollAccumulated,
 | |
| 							change)
 | |
| 							? change
 | |
| 							: (_dragOverscrollAccumulated + change));
 | |
| 				}
 | |
| 				_visibleFromDragged.fire_copy(now);
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| }
 | |
| 
 | |
| void ElasticScrollBar::mousePressEvent(QMouseEvent *e) {
 | |
| 	if (_bar.isEmpty()) {
 | |
| 		return;
 | |
| 	}
 | |
| 	toggleDragging(true);
 | |
| 	_dragPosition = e->globalPos();
 | |
| 	_dragOverscrollAccumulated = 0;
 | |
| 	if (!_overBar) {
 | |
| 		const auto start = _vertical ? _area.y() : _area.x();
 | |
| 		const auto full = _vertical ? _area.height() : _area.width();
 | |
| 		const auto bar = _vertical ? _bar.height() : _bar.width();
 | |
| 		const auto half = bar / 2;
 | |
| 		const auto middle = std::clamp(
 | |
| 			_vertical ? e->pos().y() : e->pos().x(),
 | |
| 			start + half,
 | |
| 			start + full + half - bar);
 | |
| 		const auto range = _state.visibleFrom
 | |
| 			+ (_state.fullSize - _state.visibleTill);
 | |
| 		const auto from = range * (middle - half - start) / (full - bar);
 | |
| 		_visibleFromDragged.fire_copy(from);
 | |
| 	}
 | |
| }
 | |
| 
 | |
| void ElasticScrollBar::mouseReleaseEvent(QMouseEvent *e) {
 | |
| 	toggleDragging(false);
 | |
| 	if (!_over) {
 | |
| 		setMouseTracking(false);
 | |
| 	}
 | |
| }
 | |
| 
 | |
| void ElasticScrollBar::resizeEvent(QResizeEvent *e) {
 | |
| 	refreshGeometry();
 | |
| }
 | |
| 
 | |
| bool ElasticScrollBar::eventHook(QEvent *e) {
 | |
| 	setAttribute(Qt::WA_NoMousePropagation, e->type() != QEvent::Wheel);
 | |
| 	return RpWidget::eventHook(e);
 | |
| }
 | |
| 
 | |
| ElasticScroll::ElasticScroll(
 | |
| 	QWidget *parent,
 | |
| 	const style::ScrollArea &st,
 | |
| 	Qt::Orientation orientation)
 | |
| : RpWidget(parent)
 | |
| , _st(st)
 | |
| , _bar(std::make_unique<ElasticScrollBar>(this, _st, orientation))
 | |
| , _touchTimer([=] { _touchRightButton = true; })
 | |
| , _touchScrollTimer([=] { touchScrollTimer(); })
 | |
| , _vertical(orientation == Qt::Vertical)
 | |
| , _position(Position{ 0, 0 })
 | |
| , _movement(Movement::None) {
 | |
| 	setAttribute(Qt::WA_AcceptTouchEvents);
 | |
| 
 | |
| 	_bar->visibleFromDragged(
 | |
| 	) | rpl::start_with_next([=](int from) {
 | |
| 		tryScrollTo(from, false);
 | |
| 	}, _bar->lifetime());
 | |
| }
 | |
| 
 | |
| void ElasticScroll::setHandleTouch(bool handle) {
 | |
| 	if (_touchDisabled != handle) {
 | |
| 		return;
 | |
| 	}
 | |
| 	_touchDisabled = !handle;
 | |
| 	constexpr auto attribute = Qt::WA_AcceptTouchEvents;
 | |
| 	setAttribute(attribute, handle);
 | |
| 	if (_widget) {
 | |
| 		if (handle) {
 | |
| 			_widgetAcceptsTouch = _widget->testAttribute(attribute);
 | |
| 			if (!_widgetAcceptsTouch) {
 | |
| 				_widget->setAttribute(attribute);
 | |
| 			}
 | |
| 		} else {
 | |
| 			if (!_widgetAcceptsTouch) {
 | |
| 				_widget->setAttribute(attribute, false);
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| }
 | |
| 
 | |
| bool ElasticScroll::viewportEvent(QEvent *e) {
 | |
| 	const auto type = e->type();
 | |
| 	if (type == QEvent::Wheel) {
 | |
| 		return handleWheelEvent(static_cast<QWheelEvent*>(e));
 | |
| 	} else if (type == QEvent::TouchBegin
 | |
| 		|| type == QEvent::TouchUpdate
 | |
| 		|| type == QEvent::TouchEnd
 | |
| 		|| type == QEvent::TouchCancel) {
 | |
| 		handleTouchEvent(static_cast<QTouchEvent*>(e));
 | |
| 		return true;
 | |
| 	}
 | |
| 	return false;
 | |
| }
 | |
| 
 | |
| void ElasticScroll::touchDeaccelerate(int32 elapsed) {
 | |
| 	int32 x = _touchSpeed.x();
 | |
| 	int32 y = _touchSpeed.y();
 | |
| 	_touchSpeed.setX((x == 0) ? x : (x > 0) ? qMax(0, x - elapsed) : qMin(0, x + elapsed));
 | |
| 	_touchSpeed.setY((y == 0) ? y : (y > 0) ? qMax(0, y - elapsed) : qMin(0, y + elapsed));
 | |
| }
 | |
| 
 | |
| void ElasticScroll::overscrollReturn() {
 | |
| 	_overscrollReturning = true;
 | |
| 	_ignoreMomentumFromOverscroll = _overscroll;
 | |
| 	if (overscrollFinish()) {
 | |
| 		_overscrollReturnAnimation.stop();
 | |
| 		return;
 | |
| 	} else if (_overscrollReturnAnimation.animating()) {
 | |
| 		return;
 | |
| 	}
 | |
| 	_movement = Movement::Returning;
 | |
| 	_overscrollReturnAnimation.start(
 | |
| 		[=] { applyAccumulatedScroll(); },
 | |
| 		0.,
 | |
| 		1.,
 | |
| 		kOverscrollReturnDuration,
 | |
| 		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 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 (_overscroll != currentOverscrollDefault()) {
 | |
| 		return false;
 | |
| 	}
 | |
| 	_overscrollReturning = false;
 | |
| 	_overscrollAccumulated = currentOverscrollDefaultAccumulated();
 | |
| 	_movement = Movement::None;
 | |
| 	return true;
 | |
| }
 | |
| 
 | |
| void ElasticScroll::innerResized() {
 | |
| 	_innerResizes.fire({});
 | |
| }
 | |
| 
 | |
| int ElasticScroll::scrollWidth() const {
 | |
| 	return (_vertical || !_widget)
 | |
| 		? width()
 | |
| 		: std::max(_widget->width(), width());
 | |
| }
 | |
| 
 | |
| int ElasticScroll::scrollHeight() const {
 | |
| 	return (!_vertical || !_widget)
 | |
| 		? height()
 | |
| 		: std::max(_widget->height(), height());
 | |
| }
 | |
| 
 | |
| int ElasticScroll::scrollLeftMax() const {
 | |
| 	return scrollWidth() - width();
 | |
| }
 | |
| 
 | |
| int ElasticScroll::scrollTopMax() const {
 | |
| 	return scrollHeight() - height();
 | |
| }
 | |
| 
 | |
| int ElasticScroll::scrollLeft() const {
 | |
| 	return _vertical ? 0 : _state.visibleFrom;
 | |
| }
 | |
| 
 | |
| int ElasticScroll::scrollTop() const {
 | |
| 	return _vertical ? _state.visibleFrom : 0;
 | |
| }
 | |
| 
 | |
| void ElasticScroll::touchScrollTimer() {
 | |
| 	auto nowTime = crl::now();
 | |
| 	if (_touchScrollState == TouchScrollState::Acceleration && _touchWaitingAcceleration && (nowTime - _touchAccelerationTime) > 40) {
 | |
| 		_touchScrollState = TouchScrollState::Manual;
 | |
| 		sendWheelEvent(Qt::ScrollEnd);
 | |
| 		touchResetSpeed();
 | |
| 	} else if (_touchScrollState == TouchScrollState::Auto || _touchScrollState == TouchScrollState::Acceleration) {
 | |
| 		int32 elapsed = int32(nowTime - _touchTime);
 | |
| 		QPoint delta = _touchSpeed * elapsed / 1000;
 | |
| 		sendWheelEvent(
 | |
| 			_touchPress ? Qt::ScrollUpdate : Qt::ScrollMomentum,
 | |
| 			delta);
 | |
| 
 | |
| 		if (_touchSpeed.isNull()) {
 | |
| 			_touchScrollState = TouchScrollState::Manual;
 | |
| 			sendWheelEvent(Qt::ScrollEnd);
 | |
| 			_touchScroll = false;
 | |
| 			_touchScrollTimer.cancel();
 | |
| 		} else {
 | |
| 			_touchTime = nowTime;
 | |
| 		}
 | |
| 		touchDeaccelerate(elapsed);
 | |
| 	}
 | |
| }
 | |
| 
 | |
| void ElasticScroll::touchUpdateSpeed() {
 | |
| 	const auto nowTime = crl::now();
 | |
| 	if (_touchPreviousPositionValid) {
 | |
| 		const int elapsed = nowTime - _touchSpeedTime;
 | |
| 		if (elapsed) {
 | |
| 			const QPoint newPixelDiff = (_touchPosition - _touchPreviousPosition);
 | |
| 			const QPoint pixelsPerSecond = newPixelDiff * (1000 / elapsed);
 | |
| 
 | |
| 			// fingers are inacurates, we ignore small changes to avoid stopping the autoscroll because
 | |
| 			// of a small horizontal offset when scrolling vertically
 | |
| 			const int newSpeedY = (qAbs(pixelsPerSecond.y()) > kFingerAccuracyThreshold) ? pixelsPerSecond.y() : 0;
 | |
| 			const int newSpeedX = (qAbs(pixelsPerSecond.x()) > kFingerAccuracyThreshold) ? pixelsPerSecond.x() : 0;
 | |
| 			if (_touchScrollState == TouchScrollState::Auto) {
 | |
| 				const int oldSpeedY = _touchSpeed.y();
 | |
| 				const int oldSpeedX = _touchSpeed.x();
 | |
| 				if ((oldSpeedY <= 0 && newSpeedY <= 0) || ((oldSpeedY >= 0 && newSpeedY >= 0)
 | |
| 					&& (oldSpeedX <= 0 && newSpeedX <= 0)) || (oldSpeedX >= 0 && newSpeedX >= 0)) {
 | |
| 					_touchSpeed.setY(std::clamp((oldSpeedY + (newSpeedY / 4)), -kMaxScrollAccelerated, +kMaxScrollAccelerated));
 | |
| 					_touchSpeed.setX(std::clamp((oldSpeedX + (newSpeedX / 4)), -kMaxScrollAccelerated, +kMaxScrollAccelerated));
 | |
| 				} else {
 | |
| 					_touchSpeed = QPoint();
 | |
| 				}
 | |
| 			} else {
 | |
| 				// we average the speed to avoid strange effects with the last delta
 | |
| 				if (!_touchSpeed.isNull()) {
 | |
| 					_touchSpeed.setX(std::clamp((_touchSpeed.x() / 4) + (newSpeedX * 3 / 4), -kMaxScrollFlick, +kMaxScrollFlick));
 | |
| 					_touchSpeed.setY(std::clamp((_touchSpeed.y() / 4) + (newSpeedY * 3 / 4), -kMaxScrollFlick, +kMaxScrollFlick));
 | |
| 				} else {
 | |
| 					_touchSpeed = QPoint(newSpeedX, newSpeedY);
 | |
| 				}
 | |
| 			}
 | |
| 		}
 | |
| 	} else {
 | |
| 		_touchPreviousPositionValid = true;
 | |
| 	}
 | |
| 	_touchSpeedTime = nowTime;
 | |
| 	_touchPreviousPosition = _touchPosition;
 | |
| }
 | |
| 
 | |
| void ElasticScroll::touchResetSpeed() {
 | |
| 	_touchSpeed = QPoint();
 | |
| 	_touchPreviousPositionValid = false;
 | |
| }
 | |
| 
 | |
| bool ElasticScroll::eventHook(QEvent *e) {
 | |
| 	return filterOutTouchEvent(e) || RpWidget::eventHook(e);
 | |
| }
 | |
| 
 | |
| void ElasticScroll::wheelEvent(QWheelEvent *e) {
 | |
| 	if (handleWheelEvent(e)) {
 | |
| 		e->accept();
 | |
| 	} else {
 | |
| 		e->ignore();
 | |
| 	}
 | |
| }
 | |
| 
 | |
| void ElasticScroll::paintEvent(QPaintEvent *e) {
 | |
| 	if (!_overscrollBg) {
 | |
| 		return;
 | |
| 	}
 | |
| 	const auto fillFrom = std::max(-_state.visibleFrom, 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;
 | |
| 	}
 | |
| 	auto p = QPainter(this);
 | |
| 	if (fillFrom) {
 | |
| 		p.fillRect(
 | |
| 			0,
 | |
| 			0,
 | |
| 			_vertical ? width() : fillFrom,
 | |
| 			_vertical ? fillFrom : height(),
 | |
| 			*_overscrollBg);
 | |
| 	}
 | |
| 	if (fillTill) {
 | |
| 		p.fillRect(
 | |
| 			_vertical ? 0 : (width() - fillTill),
 | |
| 			_vertical ? (height() - fillTill) : 0,
 | |
| 			_vertical ? width() : fillTill,
 | |
| 			_vertical ? fillTill : height(),
 | |
| 			*_overscrollBg);
 | |
| 	}
 | |
| }
 | |
| 
 | |
| bool ElasticScroll::handleWheelEvent(not_null<QWheelEvent*> e, bool touch) {
 | |
| 	if (_customWheelProcess
 | |
| 		&& _customWheelProcess(static_cast<QWheelEvent*>(e.get()))) {
 | |
| 		return true;
 | |
| 	}
 | |
| 	const auto phase = e->phase();
 | |
| 	const auto momentum = (phase == Qt::ScrollMomentum)
 | |
| 		|| (phase == Qt::ScrollEnd);
 | |
| 	const auto now = crl::now();
 | |
| 	const auto guard = gsl::finally([&] {
 | |
| 		_lastScroll = now;
 | |
| 	});
 | |
| 	const auto pixels = ScrollDelta(e);
 | |
| 	auto delta = _vertical ? -pixels.y() : pixels.x();
 | |
| 	if (std::abs(_vertical ? pixels.x() : pixels.y()) >= std::abs(delta)) {
 | |
| 		delta = 0;
 | |
| 	}
 | |
| 	if (_ignoreMomentumFromOverscroll) {
 | |
| 		if (!momentum) {
 | |
| 			_ignoreMomentumFromOverscroll = 0;
 | |
| 		} else if (!_overscrollReturnAnimation.animating()
 | |
| 			&& !base::OppositeSigns(_ignoreMomentumFromOverscroll, delta)) {
 | |
| 			return true;
 | |
| 		}
 | |
| 	}
 | |
| 	if (phase == Qt::NoScrollPhase) {
 | |
| 		if (_overscroll == currentOverscrollDefault()) {
 | |
| 			tryScrollTo(_state.visibleFrom + delta);
 | |
| 			_movement = Movement::None;
 | |
| 		} else if (!_overscrollReturnAnimation.animating()) {
 | |
| 			overscrollReturn();
 | |
| 		}
 | |
| 		return true;
 | |
| 	}
 | |
| 	if (!momentum) {
 | |
| 		overscrollReturnCancel();
 | |
| 	} else if (_overscroll != currentOverscrollDefault()
 | |
| 		&& !_overscrollReturnAnimation.animating()) {
 | |
| 		overscrollReturn();
 | |
| 	} else if (!_overscrollReturnAnimation.animating()) {
 | |
| 		_movement = (phase == Qt::ScrollEnd)
 | |
| 			? Movement::None
 | |
| 			: Movement::Momentum;
 | |
| 	}
 | |
| 	if (!_overscroll) {
 | |
| 		const auto normalTo = willScrollTo(_state.visibleFrom + delta);
 | |
| 		delta -= normalTo - _state.visibleFrom;
 | |
| 		applyScrollTo(normalTo);
 | |
| 	}
 | |
| 	if (!delta) {
 | |
| 		return true;
 | |
| 	}
 | |
| 	if (touch) {
 | |
| 		delta *= kTouchOverscrollMultiplier;
 | |
| 	}
 | |
| 	const auto accumulated = _overscrollAccumulated + delta;
 | |
| 	const auto type = (accumulated < 0)
 | |
| 		? _overscrollTypeFrom
 | |
| 		: (accumulated > 0)
 | |
| 		? _overscrollTypeTill
 | |
| 		: OverscrollType::None;
 | |
| 	if (type == OverscrollType::None
 | |
| 		|| base::OppositeSigns(_overscrollAccumulated, accumulated)) {
 | |
| 		_overscrollAccumulated = 0;
 | |
| 	} else {
 | |
| 		_overscrollAccumulated = accumulated;
 | |
| 	}
 | |
| 	applyAccumulatedScroll();
 | |
| 	return true;
 | |
| }
 | |
| 
 | |
| void ElasticScroll::applyAccumulatedScroll() {
 | |
| 	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) {
 | |
| 	const auto result = RpWidget::eventFilter(obj, e);
 | |
| 	if (obj == _widget.data()) {
 | |
| 		if (filterOutTouchEvent(e)) {
 | |
| 			return true;
 | |
| 		} else if (e->type() == QEvent::Resize) {
 | |
| 			const auto weak = Ui::MakeWeak(this);
 | |
| 			updateState();
 | |
| 			if (weak) {
 | |
| 				_innerResizes.fire({});
 | |
| 			}
 | |
| 		} else if (e->type() == QEvent::Move) {
 | |
| 			updateState();
 | |
| 		}
 | |
| 		return result;
 | |
| 	}
 | |
| 	return false;
 | |
| }
 | |
| 
 | |
| bool ElasticScroll::filterOutTouchEvent(QEvent *e) {
 | |
| 	const auto type = e->type();
 | |
| 	if (type == QEvent::TouchBegin
 | |
| 		|| type == QEvent::TouchUpdate
 | |
| 		|| type == QEvent::TouchEnd
 | |
| 		|| type == QEvent::TouchCancel) {
 | |
| 		const auto ev = static_cast<QTouchEvent*>(e);
 | |
| 		if (ev->device()->type() == base::TouchDevice::TouchScreen) {
 | |
| 			if (_customTouchProcess && _customTouchProcess(ev)) {
 | |
| 				return true;
 | |
| 			} else if (!_touchDisabled) {
 | |
| 				handleTouchEvent(ev);
 | |
| 				return true;
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 	return false;
 | |
| }
 | |
| 
 | |
| void ElasticScroll::handleTouchEvent(QTouchEvent *e) {
 | |
| 	if (!e->touchPoints().isEmpty()) {
 | |
| 		_touchPreviousPosition = _touchPosition;
 | |
| 		_touchPosition = e->touchPoints().cbegin()->screenPos().toPoint();
 | |
| 	}
 | |
| 
 | |
| 	switch (e->type()) {
 | |
| 	case QEvent::TouchBegin: {
 | |
| 		if (_touchPress || e->touchPoints().isEmpty()) {
 | |
| 			return;
 | |
| 		}
 | |
| 		_touchPress = true;
 | |
| 		if (_touchScrollState == TouchScrollState::Auto) {
 | |
| 			_touchScrollState = TouchScrollState::Acceleration;
 | |
| 			_touchWaitingAcceleration = true;
 | |
| 			_touchAccelerationTime = crl::now();
 | |
| 			touchUpdateSpeed();
 | |
| 			_touchStart = _touchPosition;
 | |
| 		} else {
 | |
| 			_touchScroll = false;
 | |
| 			_touchTimer.callOnce(QApplication::startDragTime());
 | |
| 		}
 | |
| 		_touchStart = _touchPreviousPosition = _touchPosition;
 | |
| 		_touchRightButton = false;
 | |
| 		sendWheelEvent(Qt::ScrollBegin);
 | |
| 	} break;
 | |
| 
 | |
| 	case QEvent::TouchUpdate: {
 | |
| 		if (!_touchPress) {
 | |
| 			return;
 | |
| 		}
 | |
| 		if (!_touchScroll
 | |
| 			&& ((_touchPosition - _touchStart).manhattanLength()
 | |
| 				>= QApplication::startDragDistance())) {
 | |
| 			_touchTimer.cancel();
 | |
| 			_touchScroll = true;
 | |
| 			touchUpdateSpeed();
 | |
| 		}
 | |
| 		if (_touchScroll) {
 | |
| 			if (_touchScrollState == TouchScrollState::Manual) {
 | |
| 				touchScrollUpdated();
 | |
| 			} else if (_touchScrollState == TouchScrollState::Acceleration) {
 | |
| 				touchUpdateSpeed();
 | |
| 				_touchAccelerationTime = crl::now();
 | |
| 				if (_touchSpeed.isNull()) {
 | |
| 					_touchScrollState = TouchScrollState::Manual;
 | |
| 				}
 | |
| 			}
 | |
| 		}
 | |
| 	} break;
 | |
| 
 | |
| 	case QEvent::TouchEnd: {
 | |
| 		if (!_touchPress) {
 | |
| 			return;
 | |
| 		}
 | |
| 		_touchPress = false;
 | |
| 		auto weak = MakeWeak(this);
 | |
| 		if (_touchScroll) {
 | |
| 			if (_touchScrollState == TouchScrollState::Manual) {
 | |
| 				_touchScrollState = TouchScrollState::Auto;
 | |
| 				_touchPreviousPositionValid = false;
 | |
| 				_touchScrollTimer.callEach(15);
 | |
| 				_touchTime = crl::now();
 | |
| 			} else if (_touchScrollState == TouchScrollState::Auto) {
 | |
| 				_touchScrollState = TouchScrollState::Manual;
 | |
| 				_touchScroll = false;
 | |
| 				touchResetSpeed();
 | |
| 			} else if (_touchScrollState == TouchScrollState::Acceleration) {
 | |
| 				_touchScrollState = TouchScrollState::Auto;
 | |
| 				_touchWaitingAcceleration = false;
 | |
| 				_touchPreviousPositionValid = false;
 | |
| 			}
 | |
| 		} else if (window()) { // one short tap -- like left mouse click, one long tap -- like right mouse click
 | |
| 			Qt::MouseButton btn(_touchRightButton ? Qt::RightButton : Qt::LeftButton);
 | |
| 
 | |
| 			if (weak) SendSynteticMouseEvent(this, QEvent::MouseMove, Qt::NoButton, _touchStart);
 | |
| 			if (weak) SendSynteticMouseEvent(this, QEvent::MouseButtonPress, btn, _touchStart);
 | |
| 			if (weak) SendSynteticMouseEvent(this, QEvent::MouseButtonRelease, btn, _touchStart);
 | |
| 
 | |
| 			if (weak && _touchRightButton) {
 | |
| 				auto windowHandle = window()->windowHandle();
 | |
| 				auto localPoint = windowHandle->mapFromGlobal(_touchStart);
 | |
| 				QContextMenuEvent ev(QContextMenuEvent::Mouse, localPoint, _touchStart, QGuiApplication::keyboardModifiers());
 | |
| 				ev.setTimestamp(crl::now());
 | |
| 				QGuiApplication::sendEvent(windowHandle, &ev);
 | |
| 			}
 | |
| 		}
 | |
| 		if (weak) {
 | |
| 			_touchTimer.cancel();
 | |
| 			_touchRightButton = false;
 | |
| 		}
 | |
| 	} break;
 | |
| 
 | |
| 	case QEvent::TouchCancel: {
 | |
| 		_touchPress = false;
 | |
| 		_touchScroll = false;
 | |
| 		_touchScrollState = TouchScrollState::Manual;
 | |
| 		_touchTimer.cancel();
 | |
| 	} break;
 | |
| 	}
 | |
| }
 | |
| 
 | |
| void ElasticScroll::touchScrollUpdated() {
 | |
| 	//touchScroll(_touchPosition - _touchPreviousPosition);
 | |
| 	const auto phase = !_touchPress
 | |
| 		? Qt::ScrollMomentum
 | |
| 		: Qt::ScrollUpdate;
 | |
| 	sendWheelEvent(phase, _touchPosition - _touchPreviousPosition);
 | |
| 	touchUpdateSpeed();
 | |
| }
 | |
| 
 | |
| void ElasticScroll::disableScroll(bool dis) {
 | |
| 	_disabled = dis;
 | |
| 	if (_disabled && _st.hiding) {
 | |
| 		_bar->toggle(false);
 | |
| 	}
 | |
| }
 | |
| 
 | |
| void ElasticScroll::updateState() {
 | |
| 	_dirtyState = false;
 | |
| 	if (!_widget) {
 | |
| 		setState({});
 | |
| 		return;
 | |
| 	}
 | |
| 	auto from = _vertical ? -_widget->y() : -_widget->x();
 | |
| 	auto till = from + (_vertical ? height() : width());
 | |
| 	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 nowOverscroll = std::max(till - nowFullSize, 0);
 | |
| 		const auto delta = std::max(
 | |
| 			std::min(nowOverscroll - wasOverscroll, from),
 | |
| 			0);
 | |
| 		if (delta) {
 | |
| 			applyScrollTo(from - delta);
 | |
| 			return;
 | |
| 		}
 | |
| 	}
 | |
| 	setState({
 | |
| 		.visibleFrom = from,
 | |
| 		.visibleTill = till,
 | |
| 		.fullSize = nowFullSize,
 | |
| 	});
 | |
| }
 | |
| 
 | |
| 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);
 | |
| 		}
 | |
| 		if (weak) {
 | |
| 			_scrolls.fire({});
 | |
| 		}
 | |
| 	}
 | |
| }
 | |
| 
 | |
| void ElasticScroll::applyScrollTo(int position, bool synthMouseMove) {
 | |
| 	if (_disabled) {
 | |
| 		return;
 | |
| 	}
 | |
| 	const auto weak = Ui::MakeWeak(this);
 | |
| 	_dirtyState = true;
 | |
| 	const auto was = _widget->geometry();
 | |
| 	_widget->move(
 | |
| 		_vertical ? _widget->x() : -position,
 | |
| 		_vertical ? -position : _widget->y());
 | |
| 	if (weak) {
 | |
| 		const auto now = _widget->geometry();
 | |
| 		const auto wasFrom = _vertical ? was.y() : was.x();
 | |
| 		const auto wasTill = wasFrom
 | |
| 			+ (_vertical ? was.height() : was.width());
 | |
| 		const auto nowFrom = _vertical ? now.y() : now.x();
 | |
| 		const auto nowTill = nowFrom
 | |
| 			+ (_vertical ? now.height() : now.width());
 | |
| 		const auto mySize = _vertical ? height() : width();
 | |
| 		if ((wasFrom > 0 && wasFrom < mySize)
 | |
| 			|| (wasTill > 0 && wasTill < mySize)
 | |
| 			|| (nowFrom > 0 && nowFrom < mySize)
 | |
| 			|| (nowTill > 0 && nowTill < mySize)) {
 | |
| 			update();
 | |
| 		}
 | |
| 		if (_dirtyState) {
 | |
| 			updateState();
 | |
| 		}
 | |
| 		if (weak && synthMouseMove) {
 | |
| 			SendSynteticMouseEvent(this, QEvent::MouseMove, Qt::NoButton);
 | |
| 		}
 | |
| 	}
 | |
| }
 | |
| 
 | |
| 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,
 | |
| 		std::min(_state.visibleFrom, 0),
 | |
| 		std::max(
 | |
| 			_state.visibleFrom,
 | |
| 			(_state.visibleFrom
 | |
| 				+ (_state.fullSize - _state.visibleTill))));
 | |
| }
 | |
| 
 | |
| void ElasticScroll::tryScrollTo(int position, bool synthMouseMove) {
 | |
| 	applyScrollTo(willScrollTo(position), synthMouseMove);
 | |
| }
 | |
| 
 | |
| void ElasticScroll::sendWheelEvent(Qt::ScrollPhase phase, QPoint delta) {
 | |
| 	auto e = QWheelEvent(
 | |
| 		mapFromGlobal(_touchPosition),
 | |
| 		_touchPosition,
 | |
| 		delta,
 | |
| 		delta,
 | |
| 		Qt::NoButton,
 | |
| 		QGuiApplication::keyboardModifiers(),
 | |
| 		phase,
 | |
| 		false,
 | |
| 		Qt::MouseEventSynthesizedByApplication);
 | |
| 	handleWheelEvent(&e, true);
 | |
| }
 | |
| 
 | |
| void ElasticScroll::resizeEvent(QResizeEvent *e) {
 | |
| 	const auto rtl = (layoutDirection() == Qt::RightToLeft);
 | |
| 	_bar->setGeometry(_vertical
 | |
| 		? QRect(
 | |
| 			(rtl ? 0 : (width() - _st.width)),
 | |
| 			0,
 | |
| 			_st.width,
 | |
| 			height())
 | |
| 		: QRect(0, height() - _st.width, width(), _st.width));
 | |
| 	_geometryChanged.fire({});
 | |
| 	updateState();
 | |
| }
 | |
| 
 | |
| void ElasticScroll::moveEvent(QMoveEvent *e) {
 | |
| 	_geometryChanged.fire({});
 | |
| }
 | |
| 
 | |
| void ElasticScroll::keyPressEvent(QKeyEvent *e) {
 | |
| 	if ((e->key() == Qt::Key_Up || e->key() == Qt::Key_Down)
 | |
| 		&& e->modifiers().testFlag(Qt::AltModifier)) {
 | |
| 		e->ignore();
 | |
| 	} else if (_widget && (e->key() == Qt::Key_Escape || e->key() == Qt::Key_Back)) {
 | |
| 		((QObject*)_widget.data())->event(e);
 | |
| 	}
 | |
| }
 | |
| 
 | |
| void ElasticScroll::enterEventHook(QEnterEvent *e) {
 | |
| 	if (!_disabled) {
 | |
| 		_bar->toggle(true);
 | |
| 	}
 | |
| }
 | |
| 
 | |
| void ElasticScroll::leaveEventHook(QEvent *e) {
 | |
| 	_bar->toggle(false);
 | |
| }
 | |
| 
 | |
| void ElasticScroll::scrollTo(ScrollToRequest request) {
 | |
| 	scrollToY(request.ymin, request.ymax);
 | |
| }
 | |
| 
 | |
| void ElasticScroll::scrollToWidget(not_null<QWidget*> widget) {
 | |
| 	if (const auto local = _widget.data()) {
 | |
| 		const auto position = Ui::MapFrom(
 | |
| 			local,
 | |
| 			widget.get(),
 | |
| 			QPoint(0, 0));
 | |
| 		const auto from = _vertical ? position.y() : position.x();
 | |
| 		const auto till = _vertical
 | |
| 			? (position.y() + widget->height())
 | |
| 			: (position.x() + widget->width());
 | |
| 		scrollToY(from, till);
 | |
| 	}
 | |
| }
 | |
| 
 | |
| void ElasticScroll::scrollToY(int toTop, int toBottom) {
 | |
| 	if (_vertical) {
 | |
| 		scrollTo(toTop, toBottom);
 | |
| 	}
 | |
| }
 | |
| 
 | |
| void ElasticScroll::scrollTo(int toFrom, int toTill) {
 | |
| 	if (const auto inner = _widget.data()) {
 | |
| 		SendPendingMoveResizeEvents(inner);
 | |
| 	}
 | |
| 	SendPendingMoveResizeEvents(this);
 | |
| 
 | |
| 	int toMin = std::min(_state.visibleFrom, 0);
 | |
| 	int toMax = std::max(
 | |
| 		_state.visibleFrom,
 | |
| 		_state.visibleFrom + _state.fullSize - _state.visibleTill);
 | |
| 	if (toFrom < toMin) {
 | |
| 		toFrom = toMin;
 | |
| 	} else if (toFrom > toMax) {
 | |
| 		toFrom = toMax;
 | |
| 	}
 | |
| 	bool exact = (toTill < 0);
 | |
| 
 | |
| 	int curFrom = _state.visibleFrom, curRange = _state.visibleTill - _state.visibleFrom, curTill = curFrom + curRange, scTo = toFrom;
 | |
| 	if (!exact && toFrom >= curFrom) {
 | |
| 		if (toTill < toFrom) toTill = toFrom;
 | |
| 		if (toTill <= curTill) return;
 | |
| 
 | |
| 		scTo = toTill - curRange;
 | |
| 		if (scTo > toFrom) scTo = toFrom;
 | |
| 		if (scTo == curFrom) return;
 | |
| 	} else {
 | |
| 		scTo = toFrom;
 | |
| 	}
 | |
| 	applyScrollTo(scTo);
 | |
| }
 | |
| 
 | |
| void ElasticScroll::doSetOwnedWidget(object_ptr<QWidget> w) {
 | |
| 	constexpr auto attribute = Qt::WA_AcceptTouchEvents;
 | |
| 	if (_widget) {
 | |
| 		_widget->removeEventFilter(this);
 | |
| 		if (!_touchDisabled && !_widgetAcceptsTouch) {
 | |
| 			_widget->setAttribute(attribute, false);
 | |
| 		}
 | |
| 	}
 | |
| 	_widget = std::move(w);
 | |
| 	if (_widget) {
 | |
| 		if (_widget->parentWidget() != this) {
 | |
| 			_widget->setParent(this);
 | |
| 			_widget->show();
 | |
| 		}
 | |
| 		_bar->raise();
 | |
| 		_widget->installEventFilter(this);
 | |
| 		if (!_touchDisabled) {
 | |
| 			_widgetAcceptsTouch = _widget->testAttribute(attribute);
 | |
| 			if (!_widgetAcceptsTouch) {
 | |
| 				_widget->setAttribute(attribute);
 | |
| 			}
 | |
| 		}
 | |
| 		updateState();
 | |
| 	}
 | |
| }
 | |
| 
 | |
| object_ptr<QWidget> ElasticScroll::doTakeWidget() {
 | |
| 	return std::move(_widget);
 | |
| }
 | |
| 
 | |
| 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 = _overscrollAccumulated = 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 = _overscrollAccumulated = 0;
 | |
| 			applyScrollTo(max);
 | |
| 			break;
 | |
| 		case OverscrollType::Virtual:
 | |
| 			applyScrollTo(max);
 | |
| 			break;
 | |
| 		case OverscrollType::Real:
 | |
| 			applyScrollTo(max + _overscroll);
 | |
| 			break;
 | |
| 		}
 | |
| 	}
 | |
| }
 | |
| 
 | |
| void ElasticScroll::setOverscrollDefaults(int from, int till, bool shift) {
 | |
| 	Expects(from <= 0 && till >= 0);
 | |
| 
 | |
| 	if (_state.visibleFrom > 0
 | |
| 		|| (!_state.visibleFrom
 | |
| 			&& _overscrollTypeFrom != OverscrollType::Virtual)) {
 | |
| 		from = 0;
 | |
| 	}
 | |
| 	if (_state.visibleTill < _state.fullSize
 | |
| 		|| (_state.visibleTill == _state.fullSize
 | |
| 			&& _overscrollTypeTill != OverscrollType::Virtual)) {
 | |
| 		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 - (shift ? 0 : _overscrollDefaultFrom))
 | |
| 			: (_overscroll - (shift ? 0 : _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();
 | |
| }
 | |
| 
 | |
| rpl::producer<> ElasticScroll::innerResizes() const {
 | |
| 	return _innerResizes.events();
 | |
| }
 | |
| 
 | |
| 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(
 | |
| 			style::ConvertScale(point.x()),
 | |
| 			style::ConvertScale(point.y()));
 | |
| 	};
 | |
| 	if (Platform::IsMac()
 | |
| 		|| (Platform::IsWindows() && e->phase() != Qt::NoScrollPhase)) {
 | |
| 		return convert(e->pixelDelta());
 | |
| 	}
 | |
| 	return convert(e->angleDelta() * style::DevicePixelRatio());
 | |
| }
 | |
| 
 | |
| } // namespace Ui
 | 
