top_peers_strip.cpp 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995
  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 "dialogs/ui/top_peers_strip.h"
  8. #include "base/event_filter.h"
  9. #include "lang/lang_keys.h"
  10. #include "ui/effects/ripple_animation.h"
  11. #include "ui/text/text.h"
  12. #include "ui/widgets/buttons.h"
  13. #include "ui/widgets/labels.h"
  14. #include "ui/widgets/menu/menu_add_action_callback_factory.h"
  15. #include "ui/widgets/popup_menu.h"
  16. #include "ui/widgets/scroll_area.h"
  17. #include "ui/dynamic_image.h"
  18. #include "ui/painter.h"
  19. #include "ui/unread_badge_paint.h"
  20. #include "styles/style_dialogs.h"
  21. #include "styles/style_widgets.h"
  22. #include <QtWidgets/QApplication>
  23. namespace Dialogs {
  24. struct TopPeersStrip::Entry {
  25. uint64 id = 0;
  26. Ui::Text::String name;
  27. std::shared_ptr<Ui::DynamicImage> userpic;
  28. std::unique_ptr<Ui::RippleAnimation> ripple;
  29. Ui::Animations::Simple onlineShown;
  30. QImage userpicFrame;
  31. float64 userpicFrameOnline = 0.;
  32. QString badgeString;
  33. uint32 badge : 27 = 0;
  34. uint32 userpicFrameDirty : 1 = 0;
  35. uint32 subscribed : 1 = 0;
  36. uint32 unread : 1 = 0;
  37. uint32 online : 1 = 0;
  38. uint32 muted : 1 = 0;
  39. };
  40. struct TopPeersStrip::Layout {
  41. int single = 0;
  42. int inrow = 0;
  43. float64 fsingle = 0.;
  44. float64 added = 0.;
  45. };
  46. TopPeersStrip::TopPeersStrip(
  47. not_null<QWidget*> parent,
  48. rpl::producer<TopPeersList> content)
  49. : RpWidget(parent)
  50. , _header(this)
  51. , _strip(this)
  52. , _selection(st::topPeersRadius, st::windowBgOver) {
  53. setupHeader();
  54. setupStrip();
  55. std::move(content) | rpl::start_with_next([=](const TopPeersList &list) {
  56. apply(list);
  57. }, lifetime());
  58. rpl::combine(
  59. _count.value(),
  60. _expanded.value()
  61. ) | rpl::start_with_next([=] {
  62. resizeToWidth(width());
  63. }, _strip.lifetime());
  64. resize(0, _header.height() + _strip.height());
  65. }
  66. void TopPeersStrip::setupHeader() {
  67. _header.resize(0, st::searchedBarHeight);
  68. const auto label = Ui::CreateChild<Ui::FlatLabel>(
  69. &_header,
  70. tr::lng_recent_frequent(),
  71. st::searchedBarLabel);
  72. const auto single = outer().width();
  73. rpl::combine(
  74. _count.value(),
  75. widthValue()
  76. ) | rpl::map(
  77. (rpl::mappers::_1 * single) > (rpl::mappers::_2 + (single * 2) / 3)
  78. ) | rpl::distinct_until_changed() | rpl::start_with_next([=](bool more) {
  79. setExpanded(false);
  80. if (!more) {
  81. const auto toggle = _toggleExpanded.current();
  82. _toggleExpanded = nullptr;
  83. delete toggle;
  84. return;
  85. } else if (_toggleExpanded.current()) {
  86. return;
  87. }
  88. const auto toggle = Ui::CreateChild<Ui::LinkButton>(
  89. &_header,
  90. tr::lng_channels_your_more(tr::now),
  91. st::searchedBarLink);
  92. toggle->show();
  93. toggle->setClickedCallback([=] {
  94. const auto expand = !_expanded.current();
  95. toggle->setText(expand
  96. ? tr::lng_recent_frequent_collapse(tr::now)
  97. : tr::lng_recent_frequent_all(tr::now));
  98. setExpanded(expand);
  99. });
  100. rpl::combine(
  101. _header.sizeValue(),
  102. toggle->widthValue()
  103. ) | rpl::start_with_next([=](QSize size, int width) {
  104. const auto x = st::searchedBarPosition.x();
  105. const auto y = st::searchedBarPosition.y();
  106. toggle->moveToRight(0, 0, size.width());
  107. label->resizeToWidth(size.width() - x - width);
  108. label->moveToLeft(x, y, size.width());
  109. }, toggle->lifetime());
  110. _toggleExpanded = toggle;
  111. }, _header.lifetime());
  112. rpl::combine(
  113. _header.sizeValue(),
  114. _toggleExpanded.value()
  115. ) | rpl::filter(
  116. rpl::mappers::_2 == nullptr
  117. ) | rpl::start_with_next([=](QSize size, const auto) {
  118. const auto x = st::searchedBarPosition.x();
  119. const auto y = st::searchedBarPosition.y();
  120. label->resizeToWidth(size.width() - x * 2);
  121. label->moveToLeft(x, y, size.width());
  122. }, _header.lifetime());
  123. _header.paintRequest() | rpl::start_with_next([=](QRect clip) {
  124. QPainter(&_header).fillRect(clip, st::searchedBarBg);
  125. }, _header.lifetime());
  126. }
  127. void TopPeersStrip::setExpanded(bool expanded) {
  128. if (_expanded.current() == expanded) {
  129. return;
  130. }
  131. const auto from = expanded ? 0. : 1.;
  132. const auto to = expanded ? 1. : 0.;
  133. _expandAnimation.start([=] {
  134. if (!_expandAnimation.animating()) {
  135. updateScrollMax();
  136. }
  137. resizeToWidth(width());
  138. update();
  139. }, from, to, st::slideDuration, anim::easeOutQuint);
  140. _expanded = expanded;
  141. }
  142. void TopPeersStrip::setupStrip() {
  143. _strip.resize(0, st::topPeers.height);
  144. _strip.setMouseTracking(true);
  145. base::install_event_filter(&_strip, [=](not_null<QEvent*> e) {
  146. const auto type = e->type();
  147. if (type == QEvent::Wheel) {
  148. stripWheelEvent(static_cast<QWheelEvent*>(e.get()));
  149. } else if (type == QEvent::MouseButtonPress) {
  150. stripMousePressEvent(static_cast<QMouseEvent*>(e.get()));
  151. } else if (type == QEvent::MouseMove) {
  152. stripMouseMoveEvent(static_cast<QMouseEvent*>(e.get()));
  153. } else if (type == QEvent::MouseButtonRelease) {
  154. stripMouseReleaseEvent(static_cast<QMouseEvent*>(e.get()));
  155. } else if (type == QEvent::ContextMenu) {
  156. stripContextMenuEvent(static_cast<QContextMenuEvent*>(e.get()));
  157. } else if (type == QEvent::Leave) {
  158. stripLeaveEvent(e.get());
  159. } else {
  160. return base::EventFilterResult::Continue;
  161. }
  162. return base::EventFilterResult::Cancel;
  163. });
  164. _strip.paintRequest() | rpl::start_with_next([=](QRect clip) {
  165. paintStrip(clip);
  166. }, _strip.lifetime());
  167. }
  168. TopPeersStrip::~TopPeersStrip() {
  169. unsubscribeUserpics(true);
  170. }
  171. int TopPeersStrip::resizeGetHeight(int newWidth) {
  172. _header.resize(newWidth, _header.height());
  173. const auto single = QSize(outer().width(), st::topPeers.height);
  174. const auto inRow = newWidth / single.width();
  175. const auto rows = (inRow > 0)
  176. ? ((std::max(_count.current(), 1) + inRow - 1) / inRow)
  177. : 1;
  178. const auto height = single.height() * rows;
  179. const auto value = _expandAnimation.value(_expanded.current() ? 1. : 0.);
  180. const auto result = anim::interpolate(single.height(), height, value);
  181. _strip.setGeometry(0, _header.height(), newWidth, result);
  182. updateScrollMax(newWidth);
  183. return _strip.y() + _strip.height();
  184. }
  185. rpl::producer<not_null<QWheelEvent*>> TopPeersStrip::verticalScrollEvents() const {
  186. return _verticalScrollEvents.events();
  187. }
  188. void TopPeersStrip::stripWheelEvent(QWheelEvent *e) {
  189. const auto phase = e->phase();
  190. const auto fullDelta = e->pixelDelta().isNull()
  191. ? e->angleDelta()
  192. : e->pixelDelta();
  193. if (phase == Qt::ScrollBegin || phase == Qt::ScrollEnd) {
  194. _scrollingLock = Qt::Orientation();
  195. if (fullDelta.isNull()) {
  196. return;
  197. }
  198. }
  199. const auto vertical = qAbs(fullDelta.x()) < qAbs(fullDelta.y());
  200. if (_scrollingLock == Qt::Orientation() && phase != Qt::NoScrollPhase) {
  201. _scrollingLock = vertical ? Qt::Vertical : Qt::Horizontal;
  202. }
  203. if (_scrollingLock == Qt::Vertical || (vertical && !_scrollLeftMax)) {
  204. _verticalScrollEvents.fire(e);
  205. return;
  206. } else if (_expandAnimation.animating()) {
  207. return;
  208. }
  209. const auto delta = vertical
  210. ? fullDelta.y()
  211. : ((style::RightToLeft() ? -1 : 1) * fullDelta.x());
  212. const auto now = _scrollLeft;
  213. const auto used = now - delta;
  214. const auto next = std::clamp(used, 0, _scrollLeftMax);
  215. if (next != now) {
  216. _scrollLeft = next;
  217. unsubscribeUserpics();
  218. updateSelected();
  219. update();
  220. }
  221. e->accept();
  222. }
  223. void TopPeersStrip::stripLeaveEvent(QEvent *e) {
  224. if (!_selectionByKeyboard) {
  225. clearSelection();
  226. }
  227. if (!_dragging) {
  228. _lastMousePosition = std::nullopt;
  229. }
  230. }
  231. void TopPeersStrip::stripMousePressEvent(QMouseEvent *e) {
  232. if (e->button() != Qt::LeftButton) {
  233. return;
  234. }
  235. _lastMousePosition = e->globalPos();
  236. _selectionByKeyboard = false;
  237. updateSelected();
  238. _mouseDownPosition = _lastMousePosition;
  239. _pressed = _selected;
  240. if (_selected >= 0) {
  241. Assert(_selected < _entries.size());
  242. auto &entry = _entries[_selected];
  243. if (!entry.ripple) {
  244. entry.ripple = std::make_unique<Ui::RippleAnimation>(
  245. st::defaultRippleAnimation,
  246. Ui::RippleAnimation::RoundRectMask(
  247. innerRounded().size(),
  248. st::topPeersRadius),
  249. [=] { update(); });
  250. }
  251. const auto layout = currentLayout();
  252. const auto expanded = _expanded.current();
  253. const auto row = expanded ? (_selected / layout.inrow) : 0;
  254. const auto column = (_selected - (row * layout.inrow));
  255. const auto x = layout.added + column * layout.fsingle - scrollLeft();
  256. const auto y = row * st::topPeers.height;
  257. entry.ripple->add(e->pos() - QPoint(
  258. x + st::topPeersMargin.left(),
  259. y + st::topPeersMargin.top()));
  260. _presses.fire_copy(entry.id);
  261. }
  262. }
  263. void TopPeersStrip::stripMouseMoveEvent(QMouseEvent *e) {
  264. if (!_lastMousePosition) {
  265. _lastMousePosition = e->globalPos();
  266. if (_selectionByKeyboard) {
  267. return;
  268. }
  269. } else if (_selectionByKeyboard
  270. && (_lastMousePosition == e->globalPos())) {
  271. return;
  272. }
  273. selectByMouse(e->globalPos());
  274. if (!_dragging && _mouseDownPosition) {
  275. if ((*_lastMousePosition - *_mouseDownPosition).manhattanLength()
  276. >= QApplication::startDragDistance()) {
  277. _pressCancelled.fire({});
  278. if (!_expandAnimation.animating()) {
  279. _dragging = true;
  280. _startDraggingLeft = _scrollLeft;
  281. }
  282. }
  283. }
  284. checkDragging();
  285. }
  286. void TopPeersStrip::selectByMouse(QPoint globalPosition) {
  287. _lastMousePosition = globalPosition;
  288. _selectionByKeyboard = false;
  289. updateSelected();
  290. }
  291. void TopPeersStrip::checkDragging() {
  292. if (_dragging && !_expandAnimation.animating()) {
  293. const auto sign = (style::RightToLeft() ? -1 : 1);
  294. const auto newLeft = std::clamp(
  295. (sign * (_mouseDownPosition->x() - _lastMousePosition->x())
  296. + _startDraggingLeft),
  297. 0,
  298. _scrollLeftMax);
  299. if (newLeft != _scrollLeft) {
  300. _scrollLeft = newLeft;
  301. unsubscribeUserpics();
  302. update();
  303. }
  304. }
  305. }
  306. void TopPeersStrip::unsubscribeUserpics(bool all) {
  307. if (!all && (_expandAnimation.animating() || _expanded.current())) {
  308. return;
  309. }
  310. const auto single = outer().width();
  311. auto x = -_scrollLeft;
  312. for (auto &entry : _entries) {
  313. if (all || x + single <= 0 || x >= width()) {
  314. if (entry.subscribed) {
  315. entry.userpic->subscribeToUpdates(nullptr);
  316. entry.subscribed = false;
  317. }
  318. entry.userpicFrame = QImage();
  319. entry.onlineShown.stop();
  320. entry.ripple = nullptr;
  321. }
  322. x += single;
  323. }
  324. }
  325. void TopPeersStrip::subscribeUserpic(Entry &entry) {
  326. const auto raw = entry.userpic.get();
  327. entry.userpic->subscribeToUpdates([=] {
  328. const auto i = ranges::find(
  329. _entries,
  330. raw,
  331. [&](const Entry &entry) { return entry.userpic.get(); });
  332. if (i != end(_entries)) {
  333. i->userpicFrameDirty = 1;
  334. }
  335. update();
  336. });
  337. entry.subscribed = true;
  338. }
  339. void TopPeersStrip::stripMouseReleaseEvent(QMouseEvent *e) {
  340. _pressCancelled.fire({});
  341. _lastMousePosition = e->globalPos();
  342. const auto guard = gsl::finally([&] {
  343. _mouseDownPosition = std::nullopt;
  344. });
  345. const auto pressed = clearPressed();
  346. if (finishDragging()) {
  347. return;
  348. }
  349. _selectionByKeyboard = false;
  350. updateSelected();
  351. if (_selected >= 0 && _selected == pressed) {
  352. Assert(_selected < _entries.size());
  353. _clicks.fire_copy(_entries[_selected].id);
  354. }
  355. }
  356. int TopPeersStrip::clearPressed() {
  357. const auto pressed = std::exchange(_pressed, -1);
  358. if (pressed >= 0) {
  359. Assert(pressed < _entries.size());
  360. auto &entry = _entries[pressed];
  361. if (entry.ripple) {
  362. entry.ripple->lastStop();
  363. }
  364. }
  365. return pressed;
  366. }
  367. void TopPeersStrip::updateScrollMax(int newWidth) {
  368. if (_expandAnimation.animating()) {
  369. return;
  370. } else if (!newWidth) {
  371. newWidth = width();
  372. }
  373. if (_expanded.current()) {
  374. _scrollLeft = 0;
  375. _scrollLeftMax = 0;
  376. } else {
  377. const auto single = outer().width();
  378. const auto widthFull = int(_entries.size()) * single;
  379. _scrollLeftMax = std::max(widthFull - newWidth, 0);
  380. _scrollLeft = std::clamp(_scrollLeft, 0, _scrollLeftMax);
  381. }
  382. unsubscribeUserpics();
  383. update();
  384. }
  385. bool TopPeersStrip::empty() const {
  386. return !_count.current();
  387. }
  388. rpl::producer<bool> TopPeersStrip::emptyValue() const {
  389. return _count.value()
  390. | rpl::map(!rpl::mappers::_1)
  391. | rpl::distinct_until_changed();
  392. }
  393. rpl::producer<uint64> TopPeersStrip::clicks() const {
  394. return _clicks.events();
  395. }
  396. rpl::producer<uint64> TopPeersStrip::pressed() const {
  397. return _presses.events();
  398. }
  399. rpl::producer<> TopPeersStrip::pressCancelled() const {
  400. return _pressCancelled.events();
  401. }
  402. void TopPeersStrip::pressLeftToContextMenu(bool shown) {
  403. if (!shown) {
  404. _contexted = -1;
  405. update();
  406. return;
  407. }
  408. _contexted = clearPressed();
  409. if (finishDragging()) {
  410. return;
  411. }
  412. _mouseDownPosition = std::nullopt;
  413. }
  414. auto TopPeersStrip::showMenuRequests() const
  415. -> rpl::producer<ShowTopPeerMenuRequest> {
  416. return _showMenuRequests.events();
  417. }
  418. auto TopPeersStrip::scrollToRequests() const
  419. -> rpl::producer<Ui::ScrollToRequest> {
  420. return _scrollToRequests.events();
  421. }
  422. void TopPeersStrip::removeLocally(uint64 id) {
  423. if (!id) {
  424. unsubscribeUserpics(true);
  425. setSelected(-1);
  426. _pressed = -1;
  427. _entries.clear();
  428. _hiddenLocally = true;
  429. _count = 0;
  430. return;
  431. }
  432. _removed.emplace(id);
  433. const auto i = ranges::find(_entries, id, &Entry::id);
  434. if (i == end(_entries)) {
  435. return;
  436. } else if (i->subscribed) {
  437. i->userpic->subscribeToUpdates(nullptr);
  438. }
  439. const auto index = int(i - begin(_entries));
  440. _entries.erase(i);
  441. if (_selected > index) {
  442. --_selected;
  443. }
  444. if (_pressed > index) {
  445. --_pressed;
  446. }
  447. if (_contexted > index) {
  448. --_contexted;
  449. }
  450. updateScrollMax();
  451. _count = int(_entries.size());
  452. update();
  453. }
  454. bool TopPeersStrip::selectedByKeyboard() const {
  455. return _selectionByKeyboard && _selected >= 0;
  456. }
  457. bool TopPeersStrip::selectByKeyboard(Qt::Key direction) {
  458. if (_entries.empty()) {
  459. return false;
  460. } else if (direction == Qt::Key()) {
  461. _selectionByKeyboard = true;
  462. if (_selected < 0) {
  463. setSelected(0);
  464. scrollToSelected();
  465. return true;
  466. }
  467. } else if (direction == Qt::Key_Left) {
  468. if (_selected > 0) {
  469. _selectionByKeyboard = true;
  470. setSelected(_selected - 1);
  471. scrollToSelected();
  472. return true;
  473. }
  474. } else if (direction == Qt::Key_Right) {
  475. if (_selected + 1 < _entries.size()) {
  476. _selectionByKeyboard = true;
  477. setSelected(_selected + 1);
  478. scrollToSelected();
  479. return true;
  480. }
  481. } else if (direction == Qt::Key_Up) {
  482. const auto layout = currentLayout();
  483. if (_selected < 0) {
  484. _selectionByKeyboard = true;
  485. const auto rows = _expanded.current()
  486. ? ((int(_entries.size()) + layout.inrow - 1) / layout.inrow)
  487. : 1;
  488. setSelected((rows - 1) * layout.inrow);
  489. scrollToSelected();
  490. return true;
  491. } else if (!_expanded.current()) {
  492. deselectByKeyboard();
  493. } else if (_selected >= 0) {
  494. const auto row = _selected / layout.inrow;
  495. if (row > 0) {
  496. _selectionByKeyboard = true;
  497. setSelected(_selected - layout.inrow);
  498. scrollToSelected();
  499. return true;
  500. } else {
  501. deselectByKeyboard();
  502. }
  503. }
  504. } else if (direction == Qt::Key_Down) {
  505. if (_selected >= 0 && _expanded.current()) {
  506. const auto layout = currentLayout();
  507. const auto row = _selected / layout.inrow;
  508. const auto rows = (int(_entries.size()) + layout.inrow - 1)
  509. / layout.inrow;
  510. if (row + 1 < rows) {
  511. _selectionByKeyboard = true;
  512. setSelected(std::min(
  513. _selected + layout.inrow,
  514. int(_entries.size()) - 1));
  515. scrollToSelected();
  516. return true;
  517. } else {
  518. deselectByKeyboard();
  519. }
  520. }
  521. }
  522. return false;
  523. }
  524. void TopPeersStrip::deselectByKeyboard() {
  525. if (_selectionByKeyboard) {
  526. setSelected(-1);
  527. }
  528. }
  529. bool TopPeersStrip::chooseRow() {
  530. if (_selected >= 0) {
  531. Assert(_selected < _entries.size());
  532. _clicks.fire_copy(_entries[_selected].id);
  533. return true;
  534. }
  535. return false;
  536. }
  537. uint64 TopPeersStrip::updateFromParentDrag(QPoint globalPosition) {
  538. if (!rect().contains(mapFromGlobal(globalPosition))) {
  539. dragLeft();
  540. return 0;
  541. }
  542. selectByMouse(globalPosition);
  543. return (_selected >= 0) ? _entries[_selected].id : 0;
  544. }
  545. void TopPeersStrip::dragLeft() {
  546. clearSelection();
  547. }
  548. void TopPeersStrip::apply(const TopPeersList &list) {
  549. if (_hiddenLocally) {
  550. return;
  551. }
  552. auto now = std::vector<Entry>();
  553. const auto selectedId = (_selected >= 0) ? _entries[_selected].id : 0;
  554. const auto pressedId = (_pressed >= 0) ? _entries[_pressed].id : 0;
  555. const auto contextedId = (_contexted >= 0) ? _entries[_contexted].id : 0;
  556. const auto restoreIndex = [&](uint64 id) {
  557. if (!id) {
  558. return -1;
  559. }
  560. const auto i = ranges::find(_entries, id, &Entry::id);
  561. return (i != end(_entries)) ? int(i - begin(_entries)) : -1;
  562. };
  563. for (const auto &entry : list.entries) {
  564. if (_removed.contains(entry.id)) {
  565. continue;
  566. }
  567. const auto i = ranges::find(_entries, entry.id, &Entry::id);
  568. if (i != end(_entries)) {
  569. now.push_back(base::take(*i));
  570. } else {
  571. now.push_back({ .id = entry.id });
  572. }
  573. apply(now.back(), entry);
  574. }
  575. if (now.empty()) {
  576. _count = 0;
  577. }
  578. for (auto &entry : _entries) {
  579. if (entry.subscribed) {
  580. entry.userpic->subscribeToUpdates(nullptr);
  581. entry.subscribed = false;
  582. }
  583. }
  584. _entries = std::move(now);
  585. _selected = restoreIndex(selectedId);
  586. _pressed = restoreIndex(pressedId);
  587. _contexted = restoreIndex(contextedId);
  588. updateScrollMax();
  589. unsubscribeUserpics();
  590. _count = int(_entries.size());
  591. update();
  592. }
  593. void TopPeersStrip::apply(Entry &entry, const TopPeersEntry &data) {
  594. Expects(entry.id == data.id);
  595. Expects(data.userpic != nullptr);
  596. if (entry.name.toString() != data.name) {
  597. entry.name.setText(st::topPeers.nameStyle, data.name);
  598. }
  599. if (entry.userpic.get() != data.userpic.get()) {
  600. if (entry.subscribed) {
  601. entry.userpic->subscribeToUpdates(nullptr);
  602. }
  603. entry.userpic = data.userpic;
  604. if (entry.subscribed) {
  605. subscribeUserpic(entry);
  606. }
  607. }
  608. if (entry.online != data.online) {
  609. entry.online = data.online;
  610. if (!entry.subscribed) {
  611. entry.onlineShown.stop();
  612. } else {
  613. entry.onlineShown.start(
  614. [=] { update(); },
  615. entry.online ? 0. : 1.,
  616. entry.online ? 1. : 0.,
  617. st::dialogsOnlineBadgeDuration);
  618. }
  619. }
  620. if (entry.badge != data.badge) {
  621. entry.badge = data.badge;
  622. entry.badgeString = QString();
  623. entry.userpicFrameDirty = 1;
  624. }
  625. if (entry.unread != data.unread) {
  626. entry.unread = data.unread;
  627. if (!entry.badge) {
  628. entry.userpicFrameDirty = 1;
  629. }
  630. }
  631. if (entry.muted != data.muted) {
  632. entry.muted = data.muted;
  633. if (entry.badge || entry.unread) {
  634. entry.userpicFrameDirty = 1;
  635. }
  636. }
  637. }
  638. QRect TopPeersStrip::outer() const {
  639. const auto &st = st::topPeers;
  640. const auto single = st.photoLeft * 2 + st.photo;
  641. return QRect(0, 0, single, st::topPeers.height);
  642. }
  643. QRect TopPeersStrip::innerRounded() const {
  644. return outer().marginsRemoved(st::topPeersMargin);
  645. }
  646. int TopPeersStrip::scrollLeft() const {
  647. const auto value = _expandAnimation.value(_expanded.current() ? 1. : 0.);
  648. return anim::interpolate(_scrollLeft, 0, value);
  649. }
  650. void TopPeersStrip::paintStrip(QRect clip) {
  651. auto p = Painter(&_strip);
  652. const auto &st = st::topPeers;
  653. const auto scroll = scrollLeft();
  654. const auto rows = (height() + st.height - 1) / st.height;
  655. const auto fromrow = std::min(clip.y() / st.height, rows);
  656. const auto tillrow = std::min(
  657. (clip.y() + clip.height() + st.height - 1) / st.height,
  658. rows);
  659. const auto layout = currentLayout();
  660. const auto fsingle = layout.fsingle;
  661. const auto added = layout.added;
  662. for (auto row = fromrow; row != tillrow; ++row) {
  663. const auto shift = scroll + row * layout.inrow * fsingle;
  664. const auto from = std::min(
  665. int(std::floor((shift + clip.x()) / fsingle)),
  666. int(_entries.size()));
  667. const auto till = std::clamp(
  668. int(std::ceil(
  669. (shift + clip.x() + clip.width() + fsingle - 1) / fsingle + 1
  670. )),
  671. from,
  672. int(_entries.size()));
  673. auto x = int(base::SafeRound(-shift + from * fsingle + added));
  674. auto y = row * st.height;
  675. const auto highlighted = (_contexted >= 0)
  676. ? _contexted
  677. : (_pressed >= 0)
  678. ? _pressed
  679. : _selected;
  680. for (auto i = from; i != till; ++i) {
  681. auto &entry = _entries[i];
  682. const auto selected = (i == highlighted);
  683. if (selected) {
  684. _selection.paint(p, innerRounded().translated(x, y));
  685. }
  686. if (entry.ripple) {
  687. entry.ripple->paint(
  688. p,
  689. x + st::topPeersMargin.left(),
  690. y + st::topPeersMargin.top(),
  691. width());
  692. if (entry.ripple->empty()) {
  693. entry.ripple = nullptr;
  694. }
  695. }
  696. if (!entry.subscribed) {
  697. subscribeUserpic(entry);
  698. }
  699. paintUserpic(p, x, y, i, selected);
  700. p.setPen(st::dialogsNameFg);
  701. entry.name.drawElided(
  702. p,
  703. x + st.nameLeft,
  704. y + st.nameTop,
  705. layout.single - 2 * st.nameLeft,
  706. 1,
  707. style::al_top);
  708. x += fsingle;
  709. }
  710. }
  711. }
  712. void TopPeersStrip::paintUserpic(
  713. Painter &p,
  714. int x,
  715. int y,
  716. int index,
  717. bool selected) {
  718. Expects(index >= 0 && index < _entries.size());
  719. auto &entry = _entries[index];
  720. const auto &st = st::topPeers;
  721. const auto size = st.photo;
  722. const auto rect = QRect(x + st.photoLeft, y + st.photoTop, size, size);
  723. const auto online = entry.onlineShown.value(entry.online ? 1. : 0.);
  724. const auto useFrame = !entry.userpicFrame.isNull()
  725. && !entry.userpicFrameDirty
  726. && (entry.userpicFrameOnline == online);
  727. if (useFrame) {
  728. p.drawImage(rect, entry.userpicFrame);
  729. return;
  730. }
  731. const auto simple = entry.userpic->image(size);
  732. const auto ratio = style::DevicePixelRatio();
  733. const auto renderFrame = (online > 0) || entry.badge || entry.unread;
  734. if (!renderFrame) {
  735. entry.userpicFrame = QImage();
  736. p.drawImage(rect, simple);
  737. return;
  738. } else if (entry.userpicFrame.size() != QSize(size, size) * ratio) {
  739. entry.userpicFrame = QImage(
  740. QSize(size, size) * ratio,
  741. QImage::Format_ARGB32_Premultiplied);
  742. entry.userpicFrame.setDevicePixelRatio(ratio);
  743. }
  744. entry.userpicFrame.fill(Qt::transparent);
  745. entry.userpicFrameDirty = 0;
  746. entry.userpicFrameOnline = online;
  747. auto q = QPainter(&entry.userpicFrame);
  748. const auto inner = QRect(0, 0, size, size);
  749. q.drawImage(inner, simple);
  750. auto hq = PainterHighQualityEnabler(q);
  751. if (online > 0) {
  752. q.setCompositionMode(QPainter::CompositionMode_Source);
  753. const auto onlineSize = st::dialogsOnlineBadgeSize;
  754. const auto stroke = st::dialogsOnlineBadgeStroke;
  755. const auto skip = st::dialogsOnlineBadgeSkip;
  756. const auto shrink = (onlineSize / 2) * (1. - online);
  757. auto pen = QPen(Qt::transparent);
  758. pen.setWidthF(stroke * online);
  759. q.setPen(pen);
  760. q.setBrush(st::dialogsOnlineBadgeFg);
  761. q.drawEllipse(QRectF(
  762. size - skip.x() - onlineSize,
  763. size - skip.y() - onlineSize,
  764. onlineSize,
  765. onlineSize
  766. ).marginsRemoved({ shrink, shrink, shrink, shrink }));
  767. q.setCompositionMode(QPainter::CompositionMode_SourceOver);
  768. }
  769. if (entry.badge || entry.unread) {
  770. if (entry.badgeString.isEmpty()) {
  771. entry.badgeString = !entry.badge
  772. ? u" "_q
  773. : (entry.badge < 1000)
  774. ? QString::number(entry.badge)
  775. : (QString::number(entry.badge / 1000) + 'K');
  776. }
  777. auto st = Ui::UnreadBadgeStyle();
  778. st.selected = selected;
  779. st.muted = entry.muted;
  780. const auto &counter = entry.badgeString;
  781. const auto badge = PaintUnreadBadge(q, counter, size, 0, st);
  782. const auto width = style::ConvertScaleExact(2.);
  783. const auto add = (width - style::ConvertScaleExact(1.)) / 2.;
  784. auto pen = QPen(Qt::transparent);
  785. pen.setWidthF(width);
  786. q.setCompositionMode(QPainter::CompositionMode_Source);
  787. q.setPen(pen);
  788. q.setBrush(Qt::NoBrush);
  789. q.drawEllipse(QRectF(badge).marginsAdded({ add, add, add, add }));
  790. }
  791. q.end();
  792. p.drawImage(rect, entry.userpicFrame);
  793. }
  794. void TopPeersStrip::stripContextMenuEvent(QContextMenuEvent *e) {
  795. _menu = nullptr;
  796. if (e->reason() == QContextMenuEvent::Mouse) {
  797. _lastMousePosition = e->globalPos();
  798. _selectionByKeyboard = false;
  799. updateSelected();
  800. }
  801. if (_selected < 0 || _entries.empty()) {
  802. return;
  803. }
  804. Assert(_selected < _entries.size());
  805. _menu = base::make_unique_q<Ui::PopupMenu>(
  806. this,
  807. st::popupMenuWithIcons);
  808. _showMenuRequests.fire({
  809. _entries[_selected].id,
  810. Ui::Menu::CreateAddActionCallback(_menu),
  811. });
  812. if (_menu->empty()) {
  813. _menu = nullptr;
  814. return;
  815. }
  816. const auto updateAfterMenuDestroyed = [=] {
  817. const auto globalPosition = QCursor::pos();
  818. if (rect().contains(mapFromGlobal(globalPosition))) {
  819. _lastMousePosition = globalPosition;
  820. _selectionByKeyboard = false;
  821. updateSelected();
  822. }
  823. };
  824. QObject::connect(
  825. _menu.get(),
  826. &QObject::destroyed,
  827. crl::guard(&_menuGuard, updateAfterMenuDestroyed));
  828. _menu->popup(e->globalPos());
  829. e->accept();
  830. }
  831. bool TopPeersStrip::finishDragging() {
  832. if (!_dragging) {
  833. return false;
  834. }
  835. checkDragging();
  836. _dragging = false;
  837. _selectionByKeyboard = false;
  838. updateSelected();
  839. return true;
  840. }
  841. TopPeersStrip::Layout TopPeersStrip::currentLayout() const {
  842. const auto single = outer().width();
  843. const auto inrow = std::max(width() / single, 1);
  844. const auto value = _expandAnimation.value(_expanded.current() ? 1. : 0.);
  845. const auto esingle = (width() / float64(inrow));
  846. const auto fsingle = single + (esingle - single) * value;
  847. return {
  848. .single = single,
  849. .inrow = inrow,
  850. .fsingle = fsingle,
  851. .added = (fsingle - single) / 2.,
  852. };
  853. }
  854. void TopPeersStrip::updateSelected() {
  855. if (_pressed >= 0 || !_lastMousePosition || _selectionByKeyboard) {
  856. return;
  857. }
  858. const auto p = _strip.mapFromGlobal(*_lastMousePosition);
  859. const auto expanded = _expanded.current();
  860. const auto row = expanded ? (p.y() / st::topPeers.height) : 0;
  861. const auto layout = currentLayout();
  862. const auto column = (_scrollLeft + p.x()) / layout.fsingle;
  863. const auto index = row * layout.inrow + int(std::floor(column));
  864. setSelected((index < 0 || index >= _entries.size()) ? -1 : index);
  865. }
  866. void TopPeersStrip::setSelected(int selected) {
  867. if (_selected != selected) {
  868. const auto over = (selected >= 0);
  869. if (over != (_selected >= 0)) {
  870. setCursor(over ? style::cur_pointer : style::cur_default);
  871. }
  872. _selected = selected;
  873. update();
  874. }
  875. }
  876. void TopPeersStrip::clearSelection() {
  877. setSelected(-1);
  878. }
  879. void TopPeersStrip::scrollToSelected() {
  880. if (_selected < 0) {
  881. return;
  882. } else if (_expanded.current()) {
  883. const auto layout = currentLayout();
  884. const auto row = _selected / layout.inrow;
  885. const auto header = _header.height();
  886. const auto top = header + row * st::topPeers.height;
  887. const auto bottom = top + st::topPeers.height;
  888. _scrollToRequests.fire({ top - (row ? 0 : header), bottom});
  889. } else {
  890. const auto single = outer().width();
  891. const auto left = _selected * single;
  892. const auto right = left + single;
  893. if (_scrollLeft > left) {
  894. _scrollLeft = std::clamp(left, 0, _scrollLeftMax);
  895. } else if (_scrollLeft + width() < right) {
  896. _scrollLeft = std::clamp(right - width(), 0, _scrollLeftMax);
  897. }
  898. const auto height = _header.height() + st::topPeers.height;
  899. _scrollToRequests.fire({ 0, height });
  900. }
  901. }
  902. } // namespace Dialogs