/* 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 "statistics/chart_widget.h" #include "base/qt/qt_key_modifiers.h" #include "statistics/linear_chart_view.h" #include "ui/abstract_button.h" #include "ui/effects/animation_value_f.h" #include "ui/rect.h" #include "styles/style_boxes.h" namespace Statistic { namespace { constexpr auto kHeightLimitsUpdateTimeout = crl::time(320); [[nodiscard]] int FindMaxValue( Data::StatisticalChart &chartData, int startXIndex, int endXIndex) { auto maxValue = 0; for (auto &l : chartData.lines) { const auto lineMax = l.segmentTree.rMaxQ(startXIndex, endXIndex); maxValue = std::max(lineMax, maxValue); } return maxValue; } [[nodiscard]] int FindMinValue( Data::StatisticalChart &chartData, int startXIndex, int endXIndex) { auto minValue = std::numeric_limits::max(); for (auto &l : chartData.lines) { const auto lineMin = l.segmentTree.rMinQ(startXIndex, endXIndex); minValue = std::min(lineMin, minValue); } return minValue; } void PaintHorizontalLines( QPainter &p, const ChartHorizontalLinesData &horizontalLine, const QRect &r) { const auto alpha = p.opacity(); p.setOpacity(horizontalLine.alpha); for (const auto &line : horizontalLine.lines) { const auto lineRect = QRect( 0, r.y() + r.height() * line.relativeValue, r.x() + r.width(), st::lineWidth); p.fillRect(lineRect, st::boxTextFg); } p.setOpacity(alpha); } void PaintCaptionsToHorizontalLines( QPainter &p, const ChartHorizontalLinesData &horizontalLine, const QRect &r) { const auto alpha = p.opacity(); p.setOpacity(horizontalLine.alpha); p.setFont(st::boxTextFont->f); p.setPen(st::boxTextFg); for (const auto &line : horizontalLine.lines) { p.drawText(10, r.y() + r.height() * line.relativeValue, line.caption); } p.setOpacity(alpha); } } // namespace class ChartWidget::Footer final : public Ui::AbstractButton { public: Footer(not_null parent); [[nodiscard]] rpl::producer xPercentageLimitsChange() const; [[nodiscard]] rpl::producer<> userInteractionFinished() const; [[nodiscard]] rpl::producer<> directionChanges() const; private: not_null _left; not_null _right; rpl::event_stream _xPercentageLimitsChange; rpl::event_stream<> _userInteractionFinished; rpl::event_stream<> _directionChanges; struct { int x = 0; int leftLimit = 0; int rightLimit = 0; int diffX = 0; } _start; }; ChartWidget::ChartAnimationController::ChartAnimationController( Fn &&updateCallback) : _animation(std::move(updateCallback)) { } void ChartWidget::ChartAnimationController::setXPercentageLimits( Data::StatisticalChart &chartData, Limits xPercentageLimits, crl::time now) { if ((_animValueXMin.to() == xPercentageLimits.min) && (_animValueXMax.to() == xPercentageLimits.max)) { return; } start(); _animValueXMin.start(xPercentageLimits.min); _animValueXMax.start(xPercentageLimits.max); _lastUserInteracted = now; { auto minY = std::numeric_limits::max(); auto maxY = 0.; auto minYIndex = 0; auto maxYIndex = 0; const auto tempXPercentage = Limits{ .min = *ranges::lower_bound( chartData.xPercentage, xPercentageLimits.min), .max = *ranges::lower_bound( chartData.xPercentage, xPercentageLimits.max), }; for (auto i = 0; i < chartData.xPercentage.size(); i++) { if (chartData.xPercentage[i] == tempXPercentage.min) { minYIndex = i; } if (chartData.xPercentage[i] == tempXPercentage.max) { maxYIndex = i; } } for (const auto &line : chartData.lines) { for (auto i = minYIndex; i < maxYIndex; i++) { if (line.y[i] > maxY) { maxY = line.y[i]; } if (line.y[i] < minY) { minY = line.y[i]; } } } _animValueYMin = anim::value( _animValueYMin.current(), minY); _animValueYMax = anim::value( _animValueYMax.current(), maxY); { auto k = (_animValueYMax.current() - _animValueYMin.current()) / float64(maxY - minY); if (k > 1.) { k = 1. / k; } constexpr auto kDtHeightSpeed1 = 0.03 / 2; constexpr auto kDtHeightSpeed2 = 0.03 / 2; constexpr auto kDtHeightSpeed3 = 0.045 / 2; constexpr auto kDtHeightSpeedThreshold1 = 0.7; constexpr auto kDtHeightSpeedThreshold2 = 0.1; constexpr auto kDtHeightInstantThreshold = 0.97; _dtYSpeed = (k > kDtHeightSpeedThreshold1) ? kDtHeightSpeed1 : (k < kDtHeightSpeedThreshold2) ? kDtHeightSpeed2 : kDtHeightSpeed3; if (k < kDtHeightInstantThreshold) { _dtCurrent = { 0., 0. }; } } } { const auto startXIndex = chartData.findStartIndex( _animValueXMin.to()); const auto endXIndex = chartData.findEndIndex( startXIndex, _animValueXMax.to()); _finalHeightLimits = { float64(FindMinValue(chartData, startXIndex, endXIndex)), float64(FindMaxValue(chartData, startXIndex, endXIndex)), }; } } void ChartWidget::ChartAnimationController::start() { if (!_animation.animating()) { _animation.start(); } } void ChartWidget::ChartAnimationController::resetAlpha() { _alphaAnimationStartedAt = 0; _animValueYAlpha = anim::value(0., 1.); } void ChartWidget::ChartAnimationController::tick( crl::time now, std::vector &horizontalLines) { if (!_animation.animating()) { return; } constexpr auto kExpandingDelay = crl::time(100); constexpr auto kXExpandingDuration = 200.; constexpr auto kAlphaExpandingDuration = 200.; if (!_yAnimationStartedAt && ((now - _lastUserInteracted) >= kExpandingDelay)) { _heightAnimationStarts.fire({}); _yAnimationStartedAt = _lastUserInteracted + kExpandingDelay; } if (!_alphaAnimationStartedAt) { _alphaAnimationStartedAt = now; } _dtCurrent.min = std::min(_dtCurrent.min + _dtYSpeed, 1.); _dtCurrent.max = std::min(_dtCurrent.max + _dtYSpeed, 1.); const auto dtX = std::min( (now - _animation.started()) / kXExpandingDuration, 1.); const auto dtAlpha = std::min( (now - _alphaAnimationStartedAt) / kAlphaExpandingDuration, 1.); const auto isFinished = [](const anim::value &anim) { return anim.current() == anim.to(); }; const auto xFinished = isFinished(_animValueXMin) && isFinished(_animValueXMax); const auto yFinished = isFinished(_animValueYMin) && isFinished(_animValueYMax); const auto alphaFinished = isFinished(_animValueYAlpha); if (xFinished && yFinished && alphaFinished) { const auto &lines = horizontalLines.back().lines; if ((lines.front().absoluteValue == _animValueYMin.to()) && (lines.back().absoluteValue == _animValueYMax.to())) { _animation.stop(); } } if (xFinished) { _animValueXMin.finish(); _animValueXMax.finish(); } else { _animValueXMin.update(dtX, anim::linear); _animValueXMax.update(dtX, anim::linear); } if (_yAnimationStartedAt) { _animValueYMin.update(_dtCurrent.min, anim::easeInCubic); _animValueYMax.update(_dtCurrent.max, anim::easeInCubic); _animValueYAlpha.update(dtAlpha, anim::easeInCubic); for (auto &horizontalLine : horizontalLines) { horizontalLine.computeRelative( _animValueYMax.current(), _animValueYMin.current()); } } if (dtAlpha >= 0. && dtAlpha <= 1.) { const auto value = _animValueYAlpha.current(); for (auto &horizontalLine : horizontalLines) { horizontalLine.alpha = horizontalLine.fixedAlpha * (1. - value); } horizontalLines.back().alpha = value; if (value == 1.) { while (horizontalLines.size() > 1) { const auto startIt = begin(horizontalLines); if (!startIt->alpha) { horizontalLines.erase(startIt); } else { break; } } } } if (yFinished && alphaFinished) { _alphaAnimationStartedAt = 0; _yAnimationStartedAt = 0; } } Limits ChartWidget::ChartAnimationController::currentXLimits() const { return { _animValueXMin.current(), _animValueXMax.current() }; } Limits ChartWidget::ChartAnimationController::currentHeightLimits() const { return { _animValueYMin.current(), _animValueYMax.current() }; } Limits ChartWidget::ChartAnimationController::finalHeightLimits() const { return _finalHeightLimits; } auto ChartWidget::ChartAnimationController::heightAnimationStarts() const -> rpl::producer<> { return _heightAnimationStarts.events(); } ChartWidget::Footer::Footer(not_null parent) : Ui::AbstractButton(parent) , _left(Ui::CreateChild(this)) , _right(Ui::CreateChild(this)) { sizeValue( ) | rpl::start_with_next([=](const QSize &s) { _left->resize(st::colorSliderWidth, s.height()); _right->resize(st::colorSliderWidth, s.height()); }, _left->lifetime()); _left->paintRequest( ) | rpl::start_with_next([=] { auto p = QPainter(_left); p.setOpacity(0.3); p.fillRect(_left->rect(), st::boxTextFg); }, _left->lifetime()); _right->paintRequest( ) | rpl::start_with_next([=] { auto p = QPainter(_right); p.setOpacity(0.3); p.fillRect(_right->rect(), st::boxTextFg); }, _right->lifetime()); _left->move(10, 0); _right->move(50, 0); const auto handleDrag = [&]( not_null side, Fn leftLimit, Fn rightLimit) { side->events( ) | rpl::filter([=](not_null e) { return (e->type() == QEvent::MouseButtonPress) || (e->type() == QEvent::MouseButtonRelease) || ((e->type() == QEvent::MouseMove) && side->isDown()); }) | rpl::start_with_next([=](not_null e) { const auto pos = static_cast(e.get())->pos(); switch (e->type()) { case QEvent::MouseMove: { const auto nowDiffXDirection = (pos.x() - _start.x) < 0; const auto wasDiffXDirection = _start.diffX < 0; if (base::IsCtrlPressed()) { const auto diff = (pos.x() - _start.x); _left->move(_left->x() + diff, side->y()); _right->move(_right->x() + diff, side->y()); } else { _start.diffX = pos.x() - _start.x; const auto nextX = std::clamp( side->x() + (pos.x() - _start.x), _start.leftLimit, _start.rightLimit); side->move(nextX, side->y()); } _xPercentageLimitsChange.fire({ .min = _left->x() / float64(width()), .max = rect::right(_right) / float64(width()), }); if (nowDiffXDirection != wasDiffXDirection) { _directionChanges.fire({}); } } break; case QEvent::MouseButtonPress: { _start.x = pos.x(); _start.leftLimit = leftLimit(); _start.rightLimit = rightLimit(); } break; case QEvent::MouseButtonRelease: { _userInteractionFinished.fire({}); _xPercentageLimitsChange.fire({ .min = _left->x() / float64(width()), .max = rect::right(_right) / float64(width()), }); _start = {}; } break; } }, side->lifetime()); }; handleDrag( _left, [=] { return 0; }, [=] { return _right->x() - _left->width(); }); handleDrag( _right, [=] { return rect::right(_left); }, [=] { return width() - _right->width(); }); } rpl::producer ChartWidget::Footer::xPercentageLimitsChange() const { return _xPercentageLimitsChange.events(); } rpl::producer<> ChartWidget::Footer::userInteractionFinished() const { return _userInteractionFinished.events(); } rpl::producer<> ChartWidget::Footer::directionChanges() const { return _directionChanges.events(); } ChartWidget::ChartWidget(not_null parent) : Ui::RpWidget(parent) , _footer(std::make_unique