1 /*
2 This file is part of Telegram Desktop,
3 the official desktop application for the Telegram messaging service.
4
5 For license and copyright information please follow this link:
6 https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
7 */
8 #include "ui/widgets/discrete_sliders.h"
9
10 #include "ui/effects/ripple_animation.h"
11 #include "styles/style_widgets.h"
12
13 namespace Ui {
14
DiscreteSlider(QWidget * parent)15 DiscreteSlider::DiscreteSlider(QWidget *parent) : RpWidget(parent) {
16 setCursor(style::cur_pointer);
17 }
18
setActiveSection(int index)19 void DiscreteSlider::setActiveSection(int index) {
20 _activeIndex = index;
21 activateCallback();
22 setSelectedSection(index);
23 }
24
activateCallback()25 void DiscreteSlider::activateCallback() {
26 if (_timerId >= 0) {
27 killTimer(_timerId);
28 _timerId = -1;
29 }
30 auto ms = crl::now();
31 if (ms >= _callbackAfterMs) {
32 _sectionActivated.fire_copy(_activeIndex);
33 } else {
34 _timerId = startTimer(_callbackAfterMs - ms, Qt::PreciseTimer);
35 }
36 }
37
timerEvent(QTimerEvent * e)38 void DiscreteSlider::timerEvent(QTimerEvent *e) {
39 activateCallback();
40 }
41
setActiveSectionFast(int index)42 void DiscreteSlider::setActiveSectionFast(int index) {
43 setActiveSection(index);
44 finishAnimating();
45 }
46
finishAnimating()47 void DiscreteSlider::finishAnimating() {
48 _a_left.stop();
49 update();
50 _callbackAfterMs = 0;
51 if (_timerId >= 0) {
52 activateCallback();
53 }
54 }
55
setSelectOnPress(bool selectOnPress)56 void DiscreteSlider::setSelectOnPress(bool selectOnPress) {
57 _selectOnPress = selectOnPress;
58 }
59
addSection(const QString & label)60 void DiscreteSlider::addSection(const QString &label) {
61 _sections.push_back(Section(label, getLabelStyle()));
62 resizeToWidth(width());
63 }
64
setSections(const std::vector<QString> & labels)65 void DiscreteSlider::setSections(const std::vector<QString> &labels) {
66 Assert(!labels.empty());
67
68 _sections.clear();
69 for (const auto &label : labels) {
70 _sections.push_back(Section(label, getLabelStyle()));
71 }
72 stopAnimation();
73 if (_activeIndex >= _sections.size()) {
74 _activeIndex = 0;
75 }
76 if (_selected >= _sections.size()) {
77 _selected = 0;
78 }
79 resizeToWidth(width());
80 }
81
getCurrentActiveLeft()82 int DiscreteSlider::getCurrentActiveLeft() {
83 const auto left = _sections.empty() ? 0 : _sections[_selected].left;
84 return _a_left.value(left);
85 }
86
87 template <typename Lambda>
enumerateSections(Lambda callback)88 void DiscreteSlider::enumerateSections(Lambda callback) {
89 for (auto §ion : _sections) {
90 if (!callback(section)) {
91 return;
92 }
93 }
94 }
95
96 template <typename Lambda>
enumerateSections(Lambda callback) const97 void DiscreteSlider::enumerateSections(Lambda callback) const {
98 for (auto §ion : _sections) {
99 if (!callback(section)) {
100 return;
101 }
102 }
103 }
104
mousePressEvent(QMouseEvent * e)105 void DiscreteSlider::mousePressEvent(QMouseEvent *e) {
106 auto index = getIndexFromPosition(e->pos());
107 if (_selectOnPress) {
108 setSelectedSection(index);
109 }
110 startRipple(index);
111 _pressed = index;
112 }
113
mouseMoveEvent(QMouseEvent * e)114 void DiscreteSlider::mouseMoveEvent(QMouseEvent *e) {
115 if (_pressed < 0) return;
116 if (_selectOnPress) {
117 setSelectedSection(getIndexFromPosition(e->pos()));
118 }
119 }
120
mouseReleaseEvent(QMouseEvent * e)121 void DiscreteSlider::mouseReleaseEvent(QMouseEvent *e) {
122 auto pressed = std::exchange(_pressed, -1);
123 if (pressed < 0) return;
124
125 auto index = getIndexFromPosition(e->pos());
126 if (pressed < _sections.size()) {
127 if (_sections[pressed].ripple) {
128 _sections[pressed].ripple->lastStop();
129 }
130 }
131 if (_selectOnPress || index == pressed) {
132 setActiveSection(index);
133 }
134 }
135
setSelectedSection(int index)136 void DiscreteSlider::setSelectedSection(int index) {
137 if (index < 0 || index >= _sections.size()) return;
138
139 if (_selected != index) {
140 auto from = _sections[_selected].left;
141 _selected = index;
142 auto to = _sections[_selected].left;
143 auto duration = getAnimationDuration();
144 _a_left.start([this] { update(); }, from, to, duration);
145 _callbackAfterMs = crl::now() + duration;
146 }
147 }
148
getIndexFromPosition(QPoint pos)149 int DiscreteSlider::getIndexFromPosition(QPoint pos) {
150 int count = _sections.size();
151 for (int i = 0; i != count; ++i) {
152 if (_sections[i].left + _sections[i].width > pos.x()) {
153 return i;
154 }
155 }
156 return count - 1;
157 }
158
Section(const QString & label,const style::TextStyle & st)159 DiscreteSlider::Section::Section(
160 const QString &label,
161 const style::TextStyle &st)
162 : label(st, label) {
163 }
164
SettingsSlider(QWidget * parent,const style::SettingsSlider & st)165 SettingsSlider::SettingsSlider(
166 QWidget *parent,
167 const style::SettingsSlider &st)
168 : DiscreteSlider(parent)
169 , _st(st) {
170 setSelectOnPress(_st.ripple.showDuration == 0);
171 }
172
setRippleTopRoundRadius(int radius)173 void SettingsSlider::setRippleTopRoundRadius(int radius) {
174 _rippleTopRoundRadius = radius;
175 }
176
getLabelStyle() const177 const style::TextStyle &SettingsSlider::getLabelStyle() const {
178 return _st.labelStyle;
179 }
180
getAnimationDuration() const181 int SettingsSlider::getAnimationDuration() const {
182 return _st.duration;
183 }
184
resizeSections(int newWidth)185 void SettingsSlider::resizeSections(int newWidth) {
186 auto count = getSectionsCount();
187 if (!count) return;
188
189 auto sectionWidths = countSectionsWidths(newWidth);
190
191 auto skip = 0;
192 auto x = 0.;
193 auto sectionWidth = sectionWidths.begin();
194 enumerateSections([&](Section §ion) {
195 Expects(sectionWidth != sectionWidths.end());
196
197 section.left = std::floor(x) + skip;
198 x += *sectionWidth;
199 section.width = qRound(x) - (section.left - skip);
200 skip += _st.barSkip;
201 ++sectionWidth;
202 return true;
203 });
204 stopAnimation();
205 }
206
countSectionsWidths(int newWidth) const207 std::vector<float64> SettingsSlider::countSectionsWidths(
208 int newWidth) const {
209 auto count = getSectionsCount();
210 auto sectionsWidth = newWidth - (count - 1) * _st.barSkip;
211 auto sectionWidth = sectionsWidth / float64(count);
212
213 auto result = std::vector<float64>(count, sectionWidth);
214 auto labelsWidth = 0;
215 auto commonWidth = true;
216 enumerateSections([&](const Section §ion) {
217 labelsWidth += section.label.maxWidth();
218 if (section.label.maxWidth() >= sectionWidth) {
219 commonWidth = false;
220 }
221 return true;
222 });
223 // If labelsWidth > sectionsWidth we're screwed anyway.
224 if (!commonWidth && labelsWidth <= sectionsWidth) {
225 auto padding = (sectionsWidth - labelsWidth) / (2. * count);
226 auto currentWidth = result.begin();
227 enumerateSections([&](const Section §ion) {
228 Expects(currentWidth != result.end());
229
230 *currentWidth = padding + section.label.maxWidth() + padding;
231 ++currentWidth;
232 return true;
233 });
234 }
235 return result;
236 }
237
resizeGetHeight(int newWidth)238 int SettingsSlider::resizeGetHeight(int newWidth) {
239 resizeSections(newWidth);
240 return _st.height;
241 }
242
startRipple(int sectionIndex)243 void SettingsSlider::startRipple(int sectionIndex) {
244 if (!_st.ripple.showDuration) return;
245 auto index = 0;
246 enumerateSections([this, &index, sectionIndex](Section §ion) {
247 if (index++ == sectionIndex) {
248 if (!section.ripple) {
249 auto mask = prepareRippleMask(sectionIndex, section);
250 section.ripple = std::make_unique<RippleAnimation>(
251 _st.ripple,
252 std::move(mask),
253 [this] { update(); });
254 }
255 const auto point = mapFromGlobal(QCursor::pos());
256 section.ripple->add(point - QPoint(section.left, 0));
257 return false;
258 }
259 return true;
260 });
261 }
262
prepareRippleMask(int sectionIndex,const Section & section)263 QImage SettingsSlider::prepareRippleMask(int sectionIndex, const Section §ion) {
264 auto size = QSize(section.width, height() - _st.rippleBottomSkip);
265 if (!_rippleTopRoundRadius || (sectionIndex > 0 && sectionIndex + 1 < getSectionsCount())) {
266 return RippleAnimation::rectMask(size);
267 }
268 return RippleAnimation::maskByDrawer(size, false, [this, sectionIndex, width = section.width](QPainter &p) {
269 auto plusRadius = _rippleTopRoundRadius + 1;
270 p.drawRoundedRect(0, 0, width, height() + plusRadius, _rippleTopRoundRadius, _rippleTopRoundRadius);
271 if (sectionIndex > 0) {
272 p.fillRect(0, 0, plusRadius, plusRadius, p.brush());
273 }
274 if (sectionIndex + 1 < getSectionsCount()) {
275 p.fillRect(width - plusRadius, 0, plusRadius, plusRadius, p.brush());
276 }
277 });
278 }
279
paintEvent(QPaintEvent * e)280 void SettingsSlider::paintEvent(QPaintEvent *e) {
281 Painter p(this);
282
283 auto clip = e->rect();
284 auto activeLeft = getCurrentActiveLeft();
285
286 enumerateSections([&](Section §ion) {
287 auto active = 1.
288 - std::clamp(
289 qAbs(activeLeft - section.left) / float64(section.width),
290 0.,
291 1.);
292 if (section.ripple) {
293 auto color = anim::color(_st.rippleBg, _st.rippleBgActive, active);
294 section.ripple->paint(p, section.left, 0, width(), &color);
295 if (section.ripple->empty()) {
296 section.ripple.reset();
297 }
298 }
299 auto from = section.left, tofill = section.width;
300 if (activeLeft > from) {
301 auto fill = qMin(tofill, activeLeft - from);
302 p.fillRect(myrtlrect(from, _st.barTop, fill, _st.barStroke), _st.barFg);
303 from += fill;
304 tofill -= fill;
305 }
306 if (activeLeft + section.width > from) {
307 if (auto fill = qMin(tofill, activeLeft + section.width - from)) {
308 p.fillRect(myrtlrect(from, _st.barTop, fill, _st.barStroke), _st.barFgActive);
309 from += fill;
310 tofill -= fill;
311 }
312 }
313 if (tofill) {
314 p.fillRect(myrtlrect(from, _st.barTop, tofill, _st.barStroke), _st.barFg);
315 }
316 if (myrtlrect(section.left, _st.labelTop, section.width, _st.labelStyle.font->height).intersects(clip)) {
317 p.setPen(anim::pen(_st.labelFg, _st.labelFgActive, active));
318 section.label.drawLeft(
319 p,
320 section.left + (section.width - section.label.maxWidth()) / 2,
321 _st.labelTop,
322 section.label.maxWidth(),
323 width());
324 }
325 return true;
326 });
327 }
328
329 } // namespace Ui
330