discrete_sliders.cpp 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485
  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 "ui/widgets/discrete_sliders.h"
  8. #include "ui/effects/ripple_animation.h"
  9. #include "styles/style_widgets.h"
  10. namespace Ui {
  11. DiscreteSlider::DiscreteSlider(QWidget *parent, bool snapToLabel)
  12. : RpWidget(parent)
  13. , _snapToLabel(snapToLabel) {
  14. setCursor(style::cur_pointer);
  15. }
  16. DiscreteSlider::~DiscreteSlider() = default;
  17. void DiscreteSlider::setActiveSection(int index) {
  18. _activeIndex = index;
  19. activateCallback();
  20. setSelectedSection(index);
  21. }
  22. void DiscreteSlider::activateCallback() {
  23. if (_timerId >= 0) {
  24. killTimer(_timerId);
  25. _timerId = -1;
  26. }
  27. auto ms = crl::now();
  28. if (ms >= _callbackAfterMs) {
  29. _sectionActivated.fire_copy(_activeIndex);
  30. } else {
  31. _timerId = startTimer(_callbackAfterMs - ms, Qt::PreciseTimer);
  32. }
  33. }
  34. void DiscreteSlider::timerEvent(QTimerEvent *e) {
  35. activateCallback();
  36. }
  37. void DiscreteSlider::setActiveSectionFast(int index) {
  38. setActiveSection(index);
  39. finishAnimating();
  40. }
  41. void DiscreteSlider::finishAnimating() {
  42. _a_left.stop();
  43. update();
  44. _callbackAfterMs = 0;
  45. if (_timerId >= 0) {
  46. activateCallback();
  47. }
  48. }
  49. void DiscreteSlider::setAdditionalContentWidthToSection(int index, int w) {
  50. if (index >= 0 && index < _sections.size()) {
  51. auto &section = _sections[index];
  52. section.contentWidth = section.label.maxWidth() + w;
  53. }
  54. }
  55. void DiscreteSlider::setSelectOnPress(bool selectOnPress) {
  56. _selectOnPress = selectOnPress;
  57. }
  58. std::vector<DiscreteSlider::Section> &DiscreteSlider::sectionsRef() {
  59. return _sections;
  60. }
  61. void DiscreteSlider::addSection(const QString &label) {
  62. _sections.push_back(Section(label, getLabelStyle()));
  63. resizeToWidth(width());
  64. }
  65. void DiscreteSlider::addSection(
  66. const TextWithEntities &label,
  67. Text::MarkedContext context) {
  68. context.repaint = [this] { update(); };
  69. _sections.push_back(Section(label, getLabelStyle(), context));
  70. resizeToWidth(width());
  71. }
  72. void DiscreteSlider::setSections(const std::vector<QString> &labels) {
  73. Assert(!labels.empty());
  74. _sections.clear();
  75. for (const auto &label : labels) {
  76. _sections.push_back(Section(label, getLabelStyle()));
  77. }
  78. refresh();
  79. }
  80. void DiscreteSlider::setSections(
  81. const std::vector<TextWithEntities> &labels,
  82. Text::MarkedContext context) {
  83. Assert(!labels.empty());
  84. context.repaint = [this] { update(); };
  85. _sections.clear();
  86. for (const auto &label : labels) {
  87. _sections.push_back(Section(label, getLabelStyle(), context));
  88. }
  89. refresh();
  90. }
  91. void DiscreteSlider::refresh() {
  92. stopAnimation();
  93. if (_activeIndex >= _sections.size()) {
  94. _activeIndex = 0;
  95. }
  96. if (_selected >= _sections.size()) {
  97. _selected = 0;
  98. }
  99. resizeToWidth(width());
  100. }
  101. DiscreteSlider::Range DiscreteSlider::getFinalActiveRange() const {
  102. const auto raw = _sections.empty() ? nullptr : &_sections[_selected];
  103. if (!raw) {
  104. return { 0, 0 };
  105. }
  106. const auto width = _snapToLabel
  107. ? std::min(raw->width, raw->contentWidth)
  108. : raw->width;
  109. return { raw->left + ((raw->width - width) / 2), width };
  110. }
  111. DiscreteSlider::Range DiscreteSlider::getCurrentActiveRange() const {
  112. const auto to = getFinalActiveRange();
  113. return {
  114. int(base::SafeRound(_a_left.value(to.left))),
  115. int(base::SafeRound(_a_width.value(to.width))),
  116. };
  117. }
  118. void DiscreteSlider::enumerateSections(Fn<bool(Section&)> callback) {
  119. for (auto &section : _sections) {
  120. if (!callback(section)) {
  121. return;
  122. }
  123. }
  124. }
  125. void DiscreteSlider::enumerateSections(
  126. Fn<bool(const Section&)> callback) const {
  127. for (const auto &section : _sections) {
  128. if (!callback(section)) {
  129. return;
  130. }
  131. }
  132. }
  133. void DiscreteSlider::mousePressEvent(QMouseEvent *e) {
  134. const auto index = getIndexFromPosition(e->pos());
  135. if (_selectOnPress) {
  136. setSelectedSection(index);
  137. }
  138. startRipple(index);
  139. _pressed = index;
  140. }
  141. void DiscreteSlider::mouseMoveEvent(QMouseEvent *e) {
  142. if (_pressed < 0) {
  143. return;
  144. }
  145. if (_selectOnPress) {
  146. setSelectedSection(getIndexFromPosition(e->pos()));
  147. }
  148. }
  149. void DiscreteSlider::mouseReleaseEvent(QMouseEvent *e) {
  150. const auto pressed = std::exchange(_pressed, -1);
  151. if (pressed < 0) {
  152. return;
  153. }
  154. const auto index = getIndexFromPosition(e->pos());
  155. if (pressed < _sections.size()) {
  156. if (_sections[pressed].ripple) {
  157. _sections[pressed].ripple->lastStop();
  158. }
  159. }
  160. if (_selectOnPress || index == pressed) {
  161. setActiveSection(index);
  162. }
  163. }
  164. void DiscreteSlider::setSelectedSection(int index) {
  165. if (index < 0 || index >= _sections.size()) {
  166. return;
  167. }
  168. if (_selected != index) {
  169. const auto from = getFinalActiveRange();
  170. _selected = index;
  171. const auto to = getFinalActiveRange();
  172. const auto duration = getAnimationDuration();
  173. const auto updater = [this] { update(); };
  174. _a_left.start(updater, from.left, to.left, duration);
  175. _a_width.start(updater, from.width, to.width, duration);
  176. _callbackAfterMs = crl::now() + duration;
  177. }
  178. }
  179. int DiscreteSlider::getIndexFromPosition(QPoint pos) {
  180. const auto count = _sections.size();
  181. for (auto i = 0; i != count; ++i) {
  182. if (_sections[i].left + _sections[i].width > pos.x()) {
  183. return i;
  184. }
  185. }
  186. return count - 1;
  187. }
  188. DiscreteSlider::Section::Section(
  189. const QString &label,
  190. const style::TextStyle &st)
  191. : label(st, label)
  192. , contentWidth(Section::label.maxWidth()) {
  193. }
  194. DiscreteSlider::Section::Section(
  195. const TextWithEntities &label,
  196. const style::TextStyle &st,
  197. const Text::MarkedContext &context) {
  198. this->label.setMarkedText(st, label, kMarkupTextOptions, context);
  199. contentWidth = Section::label.maxWidth();
  200. }
  201. SettingsSlider::SettingsSlider(
  202. QWidget *parent,
  203. const style::SettingsSlider &st)
  204. : DiscreteSlider(parent, st.barSnapToLabel)
  205. , _st(st) {
  206. if (_st.barRadius > 0) {
  207. _bar.emplace(_st.barRadius, _st.barFg);
  208. _barActive.emplace(_st.barRadius, _st.barFgActive);
  209. }
  210. setSelectOnPress(_st.ripple.showDuration == 0);
  211. }
  212. const style::SettingsSlider &SettingsSlider::st() const {
  213. return _st;
  214. }
  215. int SettingsSlider::centerOfSection(int section) const {
  216. const auto widths = countSectionsWidths(0);
  217. auto result = 0;
  218. if (section >= 0 && section < widths.size()) {
  219. for (auto i = 0; i < section; i++) {
  220. result += widths[i];
  221. }
  222. result += widths[section] / 2;
  223. }
  224. return result;
  225. }
  226. void SettingsSlider::fitWidthToSections() {
  227. const auto widths = countSectionsWidths(0);
  228. resizeToWidth(ranges::accumulate(widths, .0) + _st.padding * 2);
  229. }
  230. void SettingsSlider::setRippleTopRoundRadius(int radius) {
  231. _rippleTopRoundRadius = radius;
  232. }
  233. const style::TextStyle &SettingsSlider::getLabelStyle() const {
  234. return _st.labelStyle;
  235. }
  236. int SettingsSlider::getAnimationDuration() const {
  237. return _st.duration;
  238. }
  239. void SettingsSlider::resizeSections(int newWidth) {
  240. const auto count = getSectionsCount();
  241. if (!count) {
  242. return;
  243. }
  244. const auto sectionWidths = countSectionsWidths(newWidth);
  245. auto skip = 0;
  246. auto x = _st.padding * 1.;
  247. auto sectionWidth = sectionWidths.begin();
  248. enumerateSections([&](Section &section) {
  249. Expects(sectionWidth != sectionWidths.end());
  250. section.left = std::floor(x) + skip;
  251. x += *sectionWidth;
  252. section.width = qRound(x) - (section.left - skip);
  253. skip += _st.barSkip;
  254. ++sectionWidth;
  255. return true;
  256. });
  257. stopAnimation();
  258. }
  259. std::vector<float64> SettingsSlider::countSectionsWidths(int newWidth) const {
  260. const auto count = getSectionsCount();
  261. const auto sectionsWidth = newWidth
  262. - 2 * _st.padding
  263. - (count - 1) * _st.barSkip;
  264. const auto sectionWidth = sectionsWidth / float64(count);
  265. auto result = std::vector<float64>(count, sectionWidth);
  266. auto labelsWidth = 0;
  267. auto commonWidth = true;
  268. enumerateSections([&](const Section &section) {
  269. labelsWidth += section.contentWidth;
  270. if (section.contentWidth >= sectionWidth) {
  271. commonWidth = false;
  272. }
  273. return true;
  274. });
  275. // If labelsWidth > sectionsWidth we're screwed anyway.
  276. if (_st.strictSkip || (!commonWidth && labelsWidth <= sectionsWidth)) {
  277. auto padding = _st.strictSkip
  278. ? (_st.strictSkip / 2.)
  279. : (sectionsWidth - labelsWidth) / (2. * count);
  280. auto currentWidth = result.begin();
  281. enumerateSections([&](const Section &section) {
  282. Expects(currentWidth != result.end());
  283. *currentWidth = padding + section.contentWidth + padding;
  284. ++currentWidth;
  285. return true;
  286. });
  287. }
  288. return result;
  289. }
  290. int SettingsSlider::resizeGetHeight(int newWidth) {
  291. resizeSections(newWidth);
  292. return _st.height;
  293. }
  294. void SettingsSlider::startRipple(int sectionIndex) {
  295. if (!_st.ripple.showDuration) {
  296. return;
  297. }
  298. auto index = 0;
  299. enumerateSections([this, &index, sectionIndex](Section &section) {
  300. if (index++ == sectionIndex) {
  301. if (!section.ripple) {
  302. auto mask = prepareRippleMask(sectionIndex, section);
  303. section.ripple = std::make_unique<RippleAnimation>(
  304. _st.ripple,
  305. std::move(mask),
  306. [this] { update(); });
  307. }
  308. const auto point = mapFromGlobal(QCursor::pos());
  309. section.ripple->add(point - QPoint(section.left, 0));
  310. return false;
  311. }
  312. return true;
  313. });
  314. }
  315. QImage SettingsSlider::prepareRippleMask(
  316. int sectionIndex,
  317. const Section &section) {
  318. const auto size = QSize(section.width, height() - _st.rippleBottomSkip);
  319. if (!_rippleTopRoundRadius
  320. || (sectionIndex > 0 && sectionIndex + 1 < getSectionsCount())) {
  321. return RippleAnimation::RectMask(size);
  322. }
  323. return RippleAnimation::MaskByDrawer(size, false, [&](QPainter &p) {
  324. const auto plusRadius = _rippleTopRoundRadius + 1;
  325. p.drawRoundedRect(
  326. 0,
  327. 0,
  328. section.width,
  329. height() + plusRadius,
  330. _rippleTopRoundRadius,
  331. _rippleTopRoundRadius);
  332. if (sectionIndex > 0) {
  333. p.fillRect(0, 0, plusRadius, plusRadius, p.brush());
  334. }
  335. if (sectionIndex + 1 < getSectionsCount()) {
  336. p.fillRect(
  337. section.width - plusRadius,
  338. 0,
  339. plusRadius,
  340. plusRadius, p.brush());
  341. }
  342. });
  343. }
  344. void SettingsSlider::paintEvent(QPaintEvent *e) {
  345. auto p = QPainter(this);
  346. const auto clip = e->rect();
  347. const auto range = DiscreteSlider::getCurrentActiveRange();
  348. const auto drawRect = [&](QRect rect, bool active = false) {
  349. const auto &bar = active ? _barActive : _bar;
  350. if (bar) {
  351. bar->paint(p, rect);
  352. } else {
  353. p.fillRect(rect, active ? _st.barFgActive : _st.barFg);
  354. }
  355. };
  356. enumerateSections([&](Section &section) {
  357. const auto activeWidth = _st.barSnapToLabel
  358. ? section.contentWidth
  359. : section.width;
  360. const auto activeLeft = section.left
  361. + (section.width - activeWidth) / 2;
  362. const auto active = 1.
  363. - std::clamp(
  364. std::abs(range.left - activeLeft) / float64(range.width),
  365. 0.,
  366. 1.);
  367. if (section.ripple) {
  368. const auto color = anim::color(
  369. _st.rippleBg,
  370. _st.rippleBgActive,
  371. active);
  372. section.ripple->paint(p, section.left, 0, width(), &color);
  373. if (section.ripple->empty()) {
  374. section.ripple.reset();
  375. }
  376. }
  377. if (!_st.barSnapToLabel) {
  378. auto from = activeLeft;
  379. auto tofill = activeWidth;
  380. if (range.left > from) {
  381. const auto fill = std::min(tofill, range.left - from);
  382. drawRect(myrtlrect(from, _st.barTop, fill, _st.barStroke));
  383. from += fill;
  384. tofill -= fill;
  385. }
  386. if (range.left + activeWidth > from) {
  387. const auto fill = std::min(
  388. tofill,
  389. range.left + activeWidth - from);
  390. if (fill) {
  391. drawRect(
  392. myrtlrect(from, _st.barTop, fill, _st.barStroke),
  393. true);
  394. from += fill;
  395. tofill -= fill;
  396. }
  397. }
  398. if (tofill) {
  399. drawRect(myrtlrect(from, _st.barTop, tofill, _st.barStroke));
  400. }
  401. }
  402. const auto labelLeft = section.left
  403. + (section.width - section.contentWidth) / 2;
  404. const auto rect = myrtlrect(
  405. labelLeft,
  406. _st.labelTop,
  407. section.contentWidth,
  408. _st.labelStyle.font->height);
  409. if (rect.intersects(clip)) {
  410. p.setPen(anim::pen(_st.labelFg, _st.labelFgActive, active));
  411. section.label.draw(p, {
  412. .position = QPoint(labelLeft, _st.labelTop),
  413. .outerWidth = width(),
  414. .availableWidth = section.label.maxWidth(),
  415. });
  416. }
  417. return true;
  418. });
  419. if (_st.barSnapToLabel) {
  420. const auto add = _st.barStroke / 2;
  421. const auto from = std::max(range.left - add, 0);
  422. const auto till = std::min(range.left + range.width + add, width());
  423. if (from < till) {
  424. drawRect(
  425. myrtlrect(from, _st.barTop, till - from, _st.barStroke),
  426. true);
  427. }
  428. }
  429. }
  430. } // namespace Ui