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 &section : _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 &section : _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 &section) {
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 &section) {
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 &section) {
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 &section) {
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 &section) {
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 &section) {
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