point_details_widget.cpp 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517
  1. /*
  2. This file is part of Telegram Desktop,
  3. the official desktop application for the Telegram messaging service.
  4. For license and copyright information please follow this link:
  5. https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
  6. */
  7. #include "statistics/widgets/point_details_widget.h"
  8. #include "data/data_channel_earn.h" // Data::kEarnMultiplier.
  9. #include "info/channel_statistics/earn/earn_format.h"
  10. #include "lang/lang_keys.h"
  11. #include "statistics/statistics_common.h"
  12. #include "statistics/statistics_format_values.h"
  13. #include "statistics/statistics_graphics.h"
  14. #include "statistics/view/stack_linear_chart_common.h"
  15. #include "ui/cached_round_corners.h"
  16. #include "ui/effects/ripple_animation.h"
  17. #include "ui/painter.h"
  18. #include "ui/rect.h"
  19. #include "styles/style_layers.h"
  20. #include "styles/style_statistics.h"
  21. #include <QtCore/QDateTime>
  22. #include <QtCore/QLocale>
  23. namespace Statistic {
  24. namespace {
  25. [[nodiscard]] QString FormatWeek(float64 timestamp) {
  26. constexpr auto kSevenDays = 3600 * 24 * 7;
  27. timestamp /= 1000;
  28. return LangDayMonth(timestamp)
  29. + ' '
  30. + QChar(8212)
  31. + ' '
  32. + LangDayMonthYear(timestamp + kSevenDays);
  33. }
  34. void PaintShadow(QPainter &p, int radius, const QRect &r) {
  35. constexpr auto kHorizontalOffset = 1;
  36. constexpr auto kHorizontalOffset2 = 2;
  37. constexpr auto kVerticalOffset = 2;
  38. constexpr auto kVerticalOffset2 = 3;
  39. constexpr auto kOpacityStep = 0.2;
  40. constexpr auto kOpacityStep2 = 0.4;
  41. const auto hOffset = style::ConvertScale(kHorizontalOffset);
  42. const auto hOffset2 = style::ConvertScale(kHorizontalOffset2);
  43. const auto vOffset = style::ConvertScale(kVerticalOffset);
  44. const auto vOffset2 = style::ConvertScale(kVerticalOffset2);
  45. const auto opacity = p.opacity();
  46. auto hq = PainterHighQualityEnabler(p);
  47. p.setOpacity(opacity);
  48. p.drawRoundedRect(r + QMarginsF(0, hOffset, 0, hOffset), radius, radius);
  49. p.setOpacity(opacity * kOpacityStep);
  50. p.drawRoundedRect(r + QMarginsF(hOffset, 0, hOffset, 0), radius, radius);
  51. p.setOpacity(opacity * kOpacityStep2);
  52. p.drawRoundedRect(
  53. r + QMarginsF(hOffset2, 0, hOffset2, 0),
  54. radius,
  55. radius);
  56. p.setOpacity(opacity * kOpacityStep);
  57. p.drawRoundedRect(r + QMarginsF(0, 0, 0, vOffset), radius, radius);
  58. p.setOpacity(opacity * kOpacityStep2);
  59. p.drawRoundedRect(r + QMarginsF(0, 0, 0, vOffset2), radius, radius);
  60. p.setOpacity(opacity);
  61. }
  62. } // namespace
  63. void PaintDetails(
  64. QPainter &p,
  65. const Data::StatisticalChart::Line &line,
  66. int absoluteValue,
  67. const QRect &rect) {
  68. auto name = Ui::Text::String(
  69. st::statisticsDetailsPopupStyle,
  70. line.name);
  71. auto value = Ui::Text::String(
  72. st::statisticsDetailsPopupStyle,
  73. Lang::FormatCountDecimal(absoluteValue));
  74. const auto nameWidth = name.maxWidth();
  75. const auto valueWidth = value.maxWidth();
  76. const auto width = valueWidth
  77. + rect::m::sum::h(st::statisticsDetailsPopupMargins)
  78. + rect::m::sum::h(st::statisticsDetailsPopupPadding)
  79. + st::statisticsDetailsPopupPadding.left() // Between strings.
  80. + nameWidth;
  81. const auto height = st::statisticsDetailsPopupStyle.font->height
  82. + rect::m::sum::v(st::statisticsDetailsPopupMargins)
  83. + rect::m::sum::v(st::statisticsDetailsPopupPadding);
  84. const auto fullRect = QRect(
  85. rect.x() + rect.width() - width,
  86. rect.y(),
  87. width,
  88. height);
  89. const auto innerRect = fullRect - st::statisticsDetailsPopupPadding;
  90. const auto textRect = innerRect - st::statisticsDetailsPopupMargins;
  91. p.setBrush(st::shadowFg);
  92. p.setPen(Qt::NoPen);
  93. PaintShadow(p, st::boxRadius, innerRect);
  94. Ui::FillRoundRect(p, innerRect, st::boxBg, Ui::BoxCorners);
  95. const auto lineY = textRect.y();
  96. const auto valueContext = Ui::Text::PaintContext{
  97. .position = QPoint(rect::right(textRect) - valueWidth, lineY),
  98. .outerWidth = textRect.width(),
  99. .availableWidth = valueWidth,
  100. };
  101. const auto nameContext = Ui::Text::PaintContext{
  102. .position = QPoint(textRect.x(), lineY),
  103. .outerWidth = textRect.width(),
  104. .availableWidth = textRect.width() - valueWidth,
  105. };
  106. p.setPen(st::boxTextFg);
  107. name.draw(p, nameContext);
  108. p.setPen(line.color);
  109. value.draw(p, valueContext);
  110. }
  111. PointDetailsWidget::PointDetailsWidget(
  112. not_null<Ui::RpWidget*> parent,
  113. const Data::StatisticalChart &chartData,
  114. bool zoomEnabled)
  115. : Ui::AbstractButton(parent)
  116. , _zoomEnabled(zoomEnabled)
  117. , _chartData(chartData)
  118. , _textStyle(st::statisticsDetailsPopupStyle)
  119. , _headerStyle(st::statisticsDetailsPopupHeaderStyle) {
  120. if (zoomEnabled) {
  121. rpl::single(rpl::empty_value()) | rpl::then(
  122. style::PaletteChanged()
  123. ) | rpl::start_with_next([=] {
  124. const auto w = st::statisticsDetailsArrowShift;
  125. const auto stroke = style::ConvertScaleExact(
  126. st::statisticsDetailsArrowStroke);
  127. _arrow = QImage(
  128. QSize(w + stroke, w * 2 + stroke) * style::DevicePixelRatio(),
  129. QImage::Format_ARGB32_Premultiplied);
  130. _arrow.setDevicePixelRatio(style::DevicePixelRatio());
  131. _arrow.fill(Qt::transparent);
  132. {
  133. auto p = QPainter(&_arrow);
  134. const auto hq = PainterHighQualityEnabler(p);
  135. const auto s = stroke / 2.;
  136. p.setPen(QPen(st::windowSubTextFg, stroke));
  137. p.drawLine(QLineF(s, s, w, w + s));
  138. p.drawLine(QLineF(s, s + w * 2, w, w + s));
  139. }
  140. invalidateCache();
  141. }, lifetime());
  142. }
  143. _maxPercentageWidth = [&] {
  144. if (_chartData.hasPercentages) {
  145. const auto maxPercentageText = Ui::Text::String(
  146. _textStyle,
  147. u"10000%"_q);
  148. return maxPercentageText.maxWidth();
  149. }
  150. return 0;
  151. }();
  152. const auto hasUsdLine = (_chartData.currencyRate != 0)
  153. && (_chartData.currency != Data::StatisticalCurrency::None)
  154. && (_chartData.lines.size() == 1);
  155. const auto maxValueTextWidth = [&] {
  156. if (hasUsdLine) {
  157. auto maxValueWidth = 0;
  158. const auto multiplier = float64(Data::kEarnMultiplier);
  159. for (const auto &value : _chartData.lines.front().y) {
  160. const auto valueText = Ui::Text::String(
  161. _textStyle,
  162. Lang::FormatExactCountDecimal(value / multiplier));
  163. const auto usdText = Ui::Text::String(
  164. _textStyle,
  165. Info::ChannelEarn::ToUsd(
  166. value,
  167. _chartData.currencyRate,
  168. 0));
  169. const auto width = std::max(
  170. usdText.maxWidth(),
  171. valueText.maxWidth());
  172. if (width > maxValueWidth) {
  173. maxValueWidth = width;
  174. }
  175. }
  176. return maxValueWidth;
  177. }
  178. const auto maxAbsoluteValue = [&] {
  179. auto maxValue = ChartValue(0);
  180. for (const auto &l : _chartData.lines) {
  181. maxValue = std::max(l.maxValue, maxValue);
  182. }
  183. return maxValue;
  184. }();
  185. const auto maxValueText = Ui::Text::String(
  186. _textStyle,
  187. Lang::FormatCountDecimal(maxAbsoluteValue));
  188. return maxValueText.maxWidth();
  189. }();
  190. const auto calculatedWidth = [&]{
  191. auto maxNameTextWidth = 0;
  192. const auto isCredits
  193. = _chartData.currency == Data::StatisticalCurrency::Credits;
  194. for (const auto &dataLine : _chartData.lines) {
  195. const auto maxNameText = Ui::Text::String(
  196. _textStyle,
  197. dataLine.name);
  198. maxNameTextWidth = std::max(
  199. maxNameText.maxWidth(),
  200. maxNameTextWidth);
  201. if (hasUsdLine) {
  202. const auto text = isCredits
  203. ? tr::lng_channel_earn_chart_overriden_detail_credits
  204. : tr::lng_channel_earn_chart_overriden_detail_currency;
  205. const auto currency = Ui::Text::String(
  206. _textStyle,
  207. text(tr::now));
  208. const auto usd = Ui::Text::String(
  209. _textStyle,
  210. tr::lng_channel_earn_chart_overriden_detail_usd(
  211. tr::now));
  212. maxNameTextWidth = std::max(
  213. std::max(currency.maxWidth(), usd.maxWidth()),
  214. maxNameTextWidth);
  215. }
  216. }
  217. {
  218. const auto maxHeaderText = Ui::Text::String(
  219. _headerStyle,
  220. _chartData.weekFormat
  221. ? FormatWeek(_chartData.x.front())
  222. : LangDetailedDayMonth(_chartData.x.front() / 1000));
  223. maxNameTextWidth = std::max(
  224. maxHeaderText.maxWidth()
  225. + st::statisticsDetailsPopupPadding.left(),
  226. maxNameTextWidth);
  227. }
  228. return maxValueTextWidth
  229. + rect::m::sum::h(st::statisticsDetailsPopupMargins)
  230. + rect::m::sum::h(st::statisticsDetailsPopupPadding)
  231. + st::statisticsDetailsPopupPadding.left() // Between strings.
  232. + maxNameTextWidth
  233. + (_valueIcon.isNull()
  234. ? 0
  235. : _valueIcon.width() / style::DevicePixelRatio())
  236. + _maxPercentageWidth;
  237. }();
  238. sizeValue(
  239. ) | rpl::start_with_next([=](const QSize &s) {
  240. const auto fullRect = s.isNull()
  241. ? Rect(Size(calculatedWidth))
  242. : Rect(s);
  243. _innerRect = fullRect - st::statisticsDetailsPopupPadding;
  244. _textRect = _innerRect - st::statisticsDetailsPopupMargins;
  245. invalidateCache();
  246. }, lifetime());
  247. resize(calculatedWidth, height());
  248. resizeHeight();
  249. }
  250. void PointDetailsWidget::setLineAlpha(int lineId, float64 alpha) {
  251. for (auto &line : _lines) {
  252. if (line.id == lineId) {
  253. if (line.alpha != alpha) {
  254. line.alpha = alpha;
  255. resizeHeight();
  256. invalidateCache();
  257. update();
  258. }
  259. return;
  260. }
  261. }
  262. }
  263. void PointDetailsWidget::resizeHeight() {
  264. resize(
  265. width(),
  266. lineYAt(_chartData.lines.size() + (_chartData.currencyRate ? 1 : 0))
  267. + st::statisticsDetailsPopupMargins.bottom());
  268. }
  269. int PointDetailsWidget::xIndex() const {
  270. return _xIndex;
  271. }
  272. void PointDetailsWidget::setXIndex(int xIndex) {
  273. _xIndex = xIndex;
  274. if (xIndex < 0) {
  275. return;
  276. }
  277. {
  278. constexpr auto kOneDay = 3600 * 24 * 1000;
  279. const auto timestamp = _chartData.x[xIndex];
  280. _header.setText(
  281. _headerStyle,
  282. (timestamp < kOneDay)
  283. ? _chartData.getDayString(xIndex)
  284. : _chartData.weekFormat
  285. ? FormatWeek(timestamp)
  286. : LangDetailedDayMonth(timestamp / 1000));
  287. }
  288. _lines.clear();
  289. _lines.reserve(_chartData.lines.size());
  290. auto hasPositiveValues = false;
  291. const auto parts = _maxPercentageWidth
  292. ? PiePartsPercentageByIndices(
  293. _chartData,
  294. nullptr,
  295. { float64(xIndex), float64(xIndex) }).parts
  296. : std::vector<PiePartData::Part>();
  297. const auto multiplier = float64(Data::kEarnMultiplier);
  298. const auto isCredits
  299. = _chartData.currency == Data::StatisticalCurrency::Credits;
  300. for (auto i = 0; i < _chartData.lines.size(); i++) {
  301. const auto &dataLine = _chartData.lines[i];
  302. auto textLine = Line();
  303. textLine.id = dataLine.id;
  304. if (_maxPercentageWidth) {
  305. textLine.percentage.setText(_textStyle, parts[i].percentageText);
  306. }
  307. textLine.name.setText(_textStyle, dataLine.name);
  308. textLine.value.setText(
  309. _textStyle,
  310. Lang::FormatCountDecimal(dataLine.y[xIndex]));
  311. hasPositiveValues |= (dataLine.y[xIndex] > 0);
  312. textLine.valueColor = QColor(dataLine.color);
  313. if (_chartData.currencyRate) {
  314. auto copy = Line();
  315. copy.id = dataLine.id * 100;
  316. copy.valueColor = QColor(dataLine.color);
  317. copy.name.setText(
  318. _textStyle,
  319. (isCredits
  320. ? tr::lng_channel_earn_chart_overriden_detail_credits
  321. : tr::lng_channel_earn_chart_overriden_detail_currency)(
  322. tr::now));
  323. copy.value.setText(
  324. _textStyle,
  325. Lang::FormatExactCountDecimal(
  326. dataLine.y[xIndex] / multiplier));
  327. _lines.push_back(std::move(copy));
  328. textLine.name.setText(
  329. _textStyle,
  330. tr::lng_channel_earn_chart_overriden_detail_usd(tr::now));
  331. textLine.value.setText(
  332. _textStyle,
  333. Info::ChannelEarn::ToUsd(
  334. dataLine.y[xIndex],
  335. _chartData.currencyRate, 0));
  336. }
  337. _lines.push_back(std::move(textLine));
  338. }
  339. if (_chartData.currencyRate && _valueIcon.isNull()) {
  340. _valueIcon = ChartCurrencyIcon(_chartData, _lines.front().valueColor);
  341. }
  342. const auto clickable = _zoomEnabled && hasPositiveValues;
  343. _hasPositiveValues = hasPositiveValues;
  344. QWidget::setAttribute(
  345. Qt::WA_TransparentForMouseEvents,
  346. !clickable);
  347. invalidateCache();
  348. }
  349. void PointDetailsWidget::setAlpha(float64 alpha) {
  350. _alpha = alpha;
  351. update();
  352. }
  353. float64 PointDetailsWidget::alpha() const {
  354. return _alpha;
  355. }
  356. int PointDetailsWidget::lineYAt(int index) const {
  357. auto linesHeight = 0.;
  358. for (auto i = 0; i < index; i++) {
  359. const auto alpha = (i >= _lines.size()) ? 1. : _lines[i].alpha;
  360. linesHeight += alpha
  361. * (_textStyle.font->height
  362. + st::statisticsDetailsPopupMidLineSpace);
  363. }
  364. return _textRect.y()
  365. + _headerStyle.font->height
  366. + st::statisticsDetailsPopupMargins.bottom() / 2
  367. + std::ceil(linesHeight);
  368. }
  369. void PointDetailsWidget::invalidateCache() {
  370. _cache = QImage();
  371. }
  372. void PointDetailsWidget::mousePressEvent(QMouseEvent *e) {
  373. AbstractButton::mousePressEvent(e);
  374. const auto position = e->pos() - _innerRect.topLeft();
  375. if (!_ripple) {
  376. _ripple = std::make_unique<Ui::RippleAnimation>(
  377. st::defaultRippleAnimation,
  378. Ui::RippleAnimation::RoundRectMask(
  379. _innerRect.size(),
  380. st::boxRadius),
  381. [=] { update(); });
  382. }
  383. _ripple->add(position);
  384. }
  385. void PointDetailsWidget::mouseReleaseEvent(QMouseEvent *e) {
  386. AbstractButton::mouseReleaseEvent(e);
  387. if (_ripple) {
  388. _ripple->lastStop();
  389. }
  390. }
  391. void PointDetailsWidget::paintEvent(QPaintEvent *e) {
  392. auto painter = QPainter(this);
  393. if (_cache.isNull()) {
  394. _cache = QImage(
  395. size() * style::DevicePixelRatio(),
  396. QImage::Format_ARGB32_Premultiplied);
  397. _cache.setDevicePixelRatio(style::DevicePixelRatio());
  398. _cache.fill(Qt::transparent);
  399. auto p = QPainter(&_cache);
  400. p.setBrush(st::shadowFg);
  401. p.setPen(Qt::NoPen);
  402. PaintShadow(p, st::boxRadius, _innerRect);
  403. Ui::FillRoundRect(p, _innerRect, st::boxBg, Ui::BoxCorners);
  404. if (_ripple) {
  405. _ripple->paint(p, _innerRect.left(), _innerRect.top(), width());
  406. if (_ripple->empty()) {
  407. _ripple.reset();
  408. }
  409. }
  410. p.setPen(st::boxTextFg);
  411. const auto headerContext = Ui::Text::PaintContext{
  412. .position = _textRect.topLeft(),
  413. .availableWidth = _textRect.width(),
  414. };
  415. _header.draw(p, headerContext);
  416. for (auto i = 0; i < _lines.size(); i++) {
  417. const auto &line = _lines[i];
  418. const auto lineY = lineYAt(i);
  419. const auto valueWidth = line.value.maxWidth();
  420. const auto valueContext = Ui::Text::PaintContext{
  421. .position = QPoint(
  422. rect::right(_textRect) - valueWidth,
  423. lineY),
  424. .outerWidth = _textRect.width(),
  425. .availableWidth = valueWidth,
  426. };
  427. if (!i && !_valueIcon.isNull()) {
  428. p.drawImage(
  429. valueContext.position.x()
  430. - _valueIcon.width() / style::DevicePixelRatio(),
  431. lineY,
  432. _valueIcon);
  433. }
  434. const auto nameContext = Ui::Text::PaintContext{
  435. .position = QPoint(
  436. _textRect.x() + _maxPercentageWidth,
  437. lineY),
  438. .outerWidth = _textRect.width(),
  439. .availableWidth = _textRect.width() - valueWidth,
  440. };
  441. p.setOpacity(line.alpha * line.alpha);
  442. p.setPen(st::boxTextFg);
  443. if (_maxPercentageWidth) {
  444. const auto percentageContext = Ui::Text::PaintContext{
  445. .position = QPoint(_textRect.x(), lineY),
  446. .outerWidth = _textRect.width(),
  447. .availableWidth = _textRect.width() - valueWidth,
  448. };
  449. line.percentage.draw(p, percentageContext);
  450. }
  451. line.name.draw(p, nameContext);
  452. p.setPen(line.valueColor);
  453. line.value.draw(p, valueContext);
  454. }
  455. if (_zoomEnabled && _hasPositiveValues) {
  456. const auto s = _arrow.size() / style::DevicePixelRatio();
  457. const auto x = rect::right(_textRect) - s.width();
  458. const auto y = _textRect.y()
  459. + (_headerStyle.font->ascent - s.height());
  460. p.drawImage(x, y, _arrow);
  461. }
  462. }
  463. if (_alpha < 1.) {
  464. painter.setOpacity(_alpha);
  465. }
  466. painter.drawImage(0, 0, _cache);
  467. if (_ripple) {
  468. invalidateCache();
  469. }
  470. }
  471. } // namespace Statistic