init
Some checks failed
Docker. / Ubuntu (push) Has been cancelled
User-agent updater. / User-agent (push) Failing after 15s
Lock Threads / lock (push) Failing after 10s
Waiting for answer. / waiting-for-answer (push) Failing after 22s
Needs user action. / needs-user-action (push) Failing after 8s
Can't reproduce. / cant-reproduce (push) Failing after 8s
Close stale issues and PRs / stale (push) Has been cancelled

This commit is contained in:
allhaileris
2026-02-16 15:50:16 +03:00
commit afb81b8278
13816 changed files with 3689732 additions and 0 deletions

View File

@@ -0,0 +1,452 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "ui/widgets/chat_filters_tabs_slider.h"
#include "ui/effects/ripple_animation.h"
#include "ui/widgets/side_bar_button.h"
#include "styles/style_dialogs.h"
#include "styles/style_widgets.h"
#include <QScrollBar>
namespace Ui {
ChatsFiltersTabs::ChatsFiltersTabs(
not_null<Ui::RpWidget*> parent,
const style::SettingsSlider &st)
: Ui::SettingsSlider(parent, st)
, _st(st)
, _unreadSt([&] {
auto st = Ui::UnreadBadgeStyle();
st.align = style::al_left;
return st;
}())
, _unreadMaxString(u"99+"_q)
, _unreadSkip(st::lineWidth * 5) {
Expects(_st.barSnapToLabel && _st.strictSkip);
if (_st.barRadius > 0) {
_bar.emplace(_st.barRadius, _st.barFg);
_barActive.emplace(_st.barRadius, _st.barFgActive);
}
{
const auto one = Ui::CountUnreadBadgeSize(u"9"_q, _unreadSt, 1);
_cachedBadgeWidths = {
one.width(),
Ui::CountUnreadBadgeSize(u"99"_q, _unreadSt, 2).width(),
Ui::CountUnreadBadgeSize(u"999"_q, _unreadSt, 2).width(),
};
_cachedBadgeHeight = one.height();
}
style::PaletteChanged(
) | rpl::on_next([=] {
for (auto &[index, unread] : _unreadCounts) {
unread.cache = cacheUnreadCount(unread.count, unread.muted);
}
update();
}, lifetime());
Ui::DiscreteSlider::setSelectOnPress(false);
}
bool ChatsFiltersTabs::setSectionsAndCheckChanged(
std::vector<TextWithEntities> &&sections,
const Text::MarkedContext &context,
Fn<bool()> paused) {
const auto &was = sectionsRef();
const auto changed = [&] {
if (was.size() != sections.size()) {
return true;
}
for (auto i = 0; i < sections.size(); i++) {
if (was[i].label.toTextWithEntities() != sections[i]) {
return true;
}
}
return false;
}();
if (changed) {
Ui::DiscreteSlider::setSections(std::move(sections), context);
}
_emojiPaused = std::move(paused);
return changed;
}
void ChatsFiltersTabs::fitWidthToSections() {
SettingsSlider::fitWidthToSections();
_lockedFromX = calculateLockedFromX();
{
_sections.clear();
enumerateSections([&](Section &section) {
_sections.push_back({ not_null{ &section }, 0, false });
return true;
});
}
}
void ChatsFiltersTabs::setUnreadCount(int index, int unreadCount, bool mute) {
const auto it = _unreadCounts.find(index);
if (it == _unreadCounts.end()) {
if (unreadCount) {
_unreadCounts.emplace(index, Unread{
.cache = cacheUnreadCount(unreadCount, mute),
.count = ushort(std::clamp(
unreadCount,
0,
int(std::numeric_limits<ushort>::max()))),
.muted = mute,
});
update();
}
} else if (!unreadCount) {
_unreadCounts.erase(it);
update();
} else if (it->second.count != unreadCount || it->second.muted != mute) {
it->second.count = unreadCount;
it->second.muted = mute;
it->second.cache = cacheUnreadCount(unreadCount, mute);
update();
}
if (unreadCount) {
const auto widthIndex = (unreadCount < 10)
? 0
: (unreadCount < 100)
? 1
: 2;
setAdditionalContentWidthToSection(
index,
_cachedBadgeWidths[widthIndex] + _unreadSkip);
} else {
setAdditionalContentWidthToSection(index, 0);
}
}
int ChatsFiltersTabs::calculateLockedFromX() const {
if (!_lockedFrom) {
return std::numeric_limits<int>::max();
}
auto left = 0;
auto index = 0;
enumerateSections([&](const Section &section) {
const auto currentRight = section.left + section.width;
if (index == _lockedFrom) {
return false;
}
left = currentRight;
index++;
return true;
});
return left ? left : std::numeric_limits<int>::max();
}
void ChatsFiltersTabs::setLockedFrom(int index) {
_lockedFrom = index;
_lockedFromX = calculateLockedFromX();
if (!index) {
_paletteLifetime.destroy();
return;
}
_paletteLifetime = style::PaletteChanged(
) | rpl::on_next([this] {
_lockCache.emplace(Ui::SideBarLockIcon(_st.labelFg));
});
}
QImage ChatsFiltersTabs::cacheUnreadCount(int count, bool muted) const {
const auto widthIndex = (count < 10) ? 0 : (count < 100) ? 1 : 2;
auto image = QImage(
QSize(_cachedBadgeWidths[widthIndex], _cachedBadgeHeight)
* style::DevicePixelRatio(),
QImage::Format_ARGB32_Premultiplied);
image.setDevicePixelRatio(style::DevicePixelRatio());
image.fill(Qt::transparent);
const auto string = (count > 999)
? _unreadMaxString
: QString::number(count);
{
auto p = QPainter(&image);
if (muted) {
auto copy = _unreadSt;
copy.muted = muted;
Ui::PaintUnreadBadge(p, string, 0, 0, copy, 0);
} else {
Ui::PaintUnreadBadge(p, string, 0, 0, _unreadSt, 0);
}
}
return image;
}
void ChatsFiltersTabs::paintEvent(QPaintEvent *e) {
auto p = QPainter(this);
const auto clip = e->rect();
const auto range = getCurrentActiveRange();
const auto activeIndex = activeSection();
const auto now = crl::now();
auto index = 0;
auto raisedIndex = -1;
auto activeHorizontalShift = 0;
const auto drawSection = [&](Section &section) {
// const auto activeWidth = _st.barSnapToLabel
// ? section.contentWidth
// : section.width;
const auto horizontalShift = _sections[index].horizontalShift;
const auto shiftedLeft = section.left + horizontalShift;
if (_sections[index].raise) {
raisedIndex = index;
}
if (index == activeIndex) {
activeHorizontalShift = horizontalShift;
}
// const auto activeLeft = shiftedLeft
// + (section.width - activeWidth) / 2;
// const auto active = 1.
// - std::clamp(
// std::abs(range.left - activeLeft) / float64(range.width),
// 0.,
// 1.);
const auto active = (index == activeIndex) ? 1. : 0.;
if (section.ripple) {
const auto color = anim::color(
_st.rippleBg,
_st.rippleBgActive,
active);
section.ripple->paint(p, shiftedLeft, 0, width(), &color);
if (section.ripple->empty()) {
section.ripple.reset();
}
}
const auto labelLeft = shiftedLeft
+ (section.width - section.contentWidth) / 2;
const auto rect = myrtlrect(
labelLeft,
_st.labelTop,
section.contentWidth,
_st.labelStyle.font->height);
if (rect.intersects(clip)) {
const auto locked = (_lockedFrom && (index >= _lockedFrom));
if (locked) {
constexpr auto kPremiumLockedOpacity = 0.6;
p.setOpacity(kPremiumLockedOpacity);
}
p.setPen(anim::pen(_st.labelFg, _st.labelFgActive, active));
section.label.draw(p, {
.position = QPoint(labelLeft, _st.labelTop),
.outerWidth = width(),
.availableWidth = section.label.maxWidth(),
.now = now,
.pausedEmoji = _emojiPaused && _emojiPaused(),
});
{
const auto it = _unreadCounts.find(index);
if (it != _unreadCounts.end()) {
p.drawImage(
labelLeft
+ _unreadSkip
+ section.label.maxWidth(),
_st.labelTop,
it->second.cache);
}
}
if (locked) {
if (!_lockCache) {
_lockCache.emplace(Ui::SideBarLockIcon(_st.labelFg));
}
const auto size = _lockCache->size()
/ style::DevicePixelRatio();
p.drawImage(
labelLeft + (section.label.maxWidth() - size.width()) / 2,
height() - size.height() - st::lineWidth,
*_lockCache);
p.setOpacity(1.0);
}
}
index++;
return true;
};
enumerateSections(drawSection);
if (raisedIndex >= 0) {
index = raisedIndex;
drawSection(*_sections[raisedIndex].section);
}
if (_st.barSnapToLabel) {
const auto drawRect = [&](QRect rect, bool active) {
const auto &bar = active ? _barActive : _bar;
if (bar) {
bar->paint(p, rect);
} else {
p.fillRect(rect, active ? _st.barFgActive : _st.barFg);
}
};
const auto add = _st.barStroke / 2;
const auto from = std::max(range.left - add, 0);
const auto till = std::min(range.left + range.width + add, width());
if (from < till) {
drawRect(
myrtlrect(
from,
_st.barTop,
till - from,
_st.barStroke).translated(activeHorizontalShift, 0),
true);
}
}
}
void ChatsFiltersTabs::mousePressEvent(QMouseEvent *e) {
const auto mouseButton = e->button();
if (mouseButton == Qt::MouseButton::LeftButton) {
_lockedPressed = (e->pos().x() >= _lockedFromX);
if (_lockedPressed) {
Ui::RpWidget::mousePressEvent(e);
} else {
Ui::SettingsSlider::mousePressEvent(e);
}
} else {
Ui::RpWidget::mousePressEvent(e);
}
}
void ChatsFiltersTabs::mouseMoveEvent(QMouseEvent *e) {
if (_reordering) {
Ui::RpWidget::mouseMoveEvent(e);
} else {
Ui::SettingsSlider::mouseMoveEvent(e);
}
}
void ChatsFiltersTabs::mouseReleaseEvent(QMouseEvent *e) {
const auto mouseButton = e->button();
if (mouseButton == Qt::MouseButton::LeftButton) {
if (base::take(_lockedPressed)) {
_lockedPressed = false;
_lockedClicked.fire({});
} else {
if (_reordering) {
for (const auto &section : _sections) {
if (section.section->ripple) {
section.section->ripple->lastStop();
}
}
} else {
Ui::SettingsSlider::mouseReleaseEvent(e);
}
}
} else {
Ui::RpWidget::mouseReleaseEvent(e);
}
}
void ChatsFiltersTabs::contextMenuEvent(QContextMenuEvent *e) {
const auto pos = e->pos();
if (pos.x() >= _lockedFromX) {
return;
}
auto left = 0;
auto index = 0;
enumerateSections([&](const Section &section) {
const auto currentRight = section.left + section.width;
if (pos.x() > left && pos.x() < currentRight) {
return false;
}
left = currentRight;
index++;
return true;
});
_contextMenuRequested.fire_copy(index);
}
rpl::producer<int> ChatsFiltersTabs::contextMenuRequested() const {
return _contextMenuRequested.events();
}
rpl::producer<> ChatsFiltersTabs::lockedClicked() const {
return _lockedClicked.events();
}
int ChatsFiltersTabs::count() const {
return _sections.size();
}
void ChatsFiltersTabs::setHorizontalShift(int index, int shift) {
Expects(index >= 0 && index < _sections.size());
auto &section = _sections[index];
if (shift - section.horizontalShift) {
section.horizontalShift = shift;
update();
}
}
void ChatsFiltersTabs::setRaised(int index) {
_sections[index].raise = true;
update();
}
void ChatsFiltersTabs::reorderSections(int oldIndex, int newIndex) {
Expects(oldIndex >= 0 && oldIndex < _sections.size());
Expects(newIndex >= 0 && newIndex < _sections.size());
// Expects(!_inResize);
auto lefts = std::vector<int>();
enumerateSections([&](Section &section) {
lefts.emplace_back(section.left);
return true;
});
const auto wasActive = activeSection();
{
auto unreadCounts = base::flat_map<Index, Unread>();
for (auto &[index, unread] : _unreadCounts) {
unreadCounts.emplace(
base::reorder_index(index, oldIndex, newIndex),
std::move(unread));
}
_unreadCounts = std::move(unreadCounts);
}
base::reorder(sectionsRef(), oldIndex, newIndex);
Ui::DiscreteSlider::setActiveSectionFast(
base::reorder_index(wasActive, oldIndex, newIndex));
Ui::DiscreteSlider::stopAnimation();
{
_sections.clear();
auto left = 0;
enumerateSections([&](Section &section) {
_sections.push_back({ not_null{ &section }, 0, false });
section.left = left;
left += section.width;
return true;
});
}
update();
}
not_null<Ui::DiscreteSlider::Section*> ChatsFiltersTabs::widgetAt(
int index) const {
Expects(index >= 0 && index < count());
return _sections[index].section;
}
void ChatsFiltersTabs::setReordering(int value) {
_reordering = value;
}
int ChatsFiltersTabs::reordering() const {
return _reordering;
}
void ChatsFiltersTabs::stopAnimation() {
Ui::DiscreteSlider::stopAnimation();
}
} // namespace Ui

View File

@@ -0,0 +1,101 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
#include "ui/unread_badge_paint.h"
#include "ui/widgets/discrete_sliders.h"
namespace style {
struct SettingsSlider;
} // namespace style
namespace Ui {
class RpWidget;
class SettingsSlider;
class ChatsFiltersTabsReorder;
class ChatsFiltersTabs final : public Ui::SettingsSlider {
public:
ChatsFiltersTabs(
not_null<Ui::RpWidget*> parent,
const style::SettingsSlider &st);
bool setSectionsAndCheckChanged(
std::vector<TextWithEntities> &&sections,
const Text::MarkedContext &context,
Fn<bool()> paused);
void fitWidthToSections() override;
void setUnreadCount(int index, int unreadCount, bool muted);
void setLockedFrom(int index);
[[nodiscard]] rpl::producer<int> contextMenuRequested() const;
[[nodiscard]] rpl::producer<> lockedClicked() const;
void setHorizontalShift(int index, int shift);
void setRaised(int index);
[[nodiscard]] int count() const;
void reorderSections(int oldIndex, int newIndex);
[[nodiscard]] not_null<DiscreteSlider::Section*> widgetAt(int i) const;
void setReordering(int value);
[[nodiscard]] int reordering() const;
void stopAnimation();
protected:
struct ShiftedSection {
not_null<Ui::DiscreteSlider::Section*> section;
int horizontalShift = 0;
bool raise = false;
};
friend class ChatsFiltersTabsReorder;
void paintEvent(QPaintEvent *e) override;
void mousePressEvent(QMouseEvent *e) override;
void mouseMoveEvent(QMouseEvent *e) override;
void mouseReleaseEvent(QMouseEvent *e) override;
void contextMenuEvent(QContextMenuEvent *e) override;
std::vector<ShiftedSection> _sections;
private:
[[nodiscard]] QImage cacheUnreadCount(int count, bool muted) const;
[[nodiscard]] int calculateLockedFromX() const;
using Index = int;
struct Unread final {
QImage cache;
ushort count = 0;
bool muted = false;
};
base::flat_map<Index, Unread> _unreadCounts;
const style::SettingsSlider &_st;
const UnreadBadgeStyle _unreadSt;
const QString _unreadMaxString;
const int _unreadSkip;
std::vector<int> _cachedBadgeWidths;
int _cachedBadgeHeight = 0;
int _lockedFrom = 0;
int _lockedFromX = 0;
bool _lockedPressed = false;
std::optional<Ui::RoundRect> _bar;
std::optional<Ui::RoundRect> _barActive;
std::optional<QImage> _lockCache;
Fn<bool()> _emojiPaused;
int _reordering = 0;
rpl::lifetime _paletteLifetime;
rpl::event_stream<int> _contextMenuRequested;
rpl::event_stream<> _lockedClicked;
};
} // namespace Ui

View File

@@ -0,0 +1,376 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "ui/widgets/chat_filters_tabs_slider_reorder.h"
#include "ui/widgets/scroll_area.h"
#include "styles/style_basic.h"
#include <QScrollBar>
#include <QtGui/QtEvents>
#include <QtWidgets/QApplication>
namespace Ui {
namespace {
constexpr auto kScrollFactor = 0.05;
} // namespace
ChatsFiltersTabsReorder::ChatsFiltersTabsReorder(
not_null<ChatsFiltersTabs*> layout,
not_null<ScrollArea*> scroll)
: _layout(layout)
, _scroll(scroll)
, _scrollAnimation([this] { updateScrollCallback(); }) {
}
ChatsFiltersTabsReorder::ChatsFiltersTabsReorder(
not_null<ChatsFiltersTabs*> layout)
: _layout(layout) {
}
void ChatsFiltersTabsReorder::cancel() {
if (_currentWidget) {
cancelCurrent(indexOf(_currentWidget));
}
_lifetime.destroy();
for (auto i = 0, count = _layout->count(); i != count; ++i) {
_layout->setHorizontalShift(i, 0);
}
_entries.clear();
}
void ChatsFiltersTabsReorder::start() {
const auto count = _layout->count();
if (count < 2) {
return;
}
_layout->events()
| rpl::on_next_done([this](not_null<QEvent*> e) {
switch (e->type()) {
case QEvent::MouseMove:
mouseMove(static_cast<QMouseEvent*>(e.get())->globalPos());
break;
case QEvent::MouseButtonPress: {
const auto m = static_cast<QMouseEvent*>(e.get());
mousePress(m->button(), m->pos(), m->globalPos());
break;
}
case QEvent::MouseButtonRelease:
mouseRelease(static_cast<QMouseEvent*>(e.get())->button());
break;
}
}, [this] {
cancel();
}, _lifetime);
for (auto i = 0; i != count; ++i) {
const auto widget = _layout->widgetAt(i);
_entries.push_back({ widget });
}
}
void ChatsFiltersTabsReorder::addPinnedInterval(int from, int length) {
_pinnedIntervals.push_back({ from, length });
}
void ChatsFiltersTabsReorder::clearPinnedIntervals() {
_pinnedIntervals.clear();
}
bool ChatsFiltersTabsReorder::Interval::isIn(int index) const {
return (index >= from) && (index < (from + length));
}
bool ChatsFiltersTabsReorder::isIndexPinned(int index) const {
return ranges::any_of(_pinnedIntervals, [&](const Interval &i) {
return i.isIn(index);
});
}
void ChatsFiltersTabsReorder::checkForStart(QPoint position) {
const auto shift = position.x() - _currentStart;
const auto delta = QApplication::startDragDistance();
if (std::abs(shift) <= delta) {
return;
}
_currentState = State::Started;
_currentStart += (shift > 0) ? delta : -delta;
const auto index = indexOf(_currentWidget);
_layout->setRaised(index);
_currentDesiredIndex = index;
_updates.fire({ _currentWidget, index, index, _currentState });
updateOrder(index, position);
}
void ChatsFiltersTabsReorder::updateOrder(int index, QPoint position) {
if (isIndexPinned(index)) {
return;
}
const auto shift = position.x() - _currentStart;
auto &current = _entries[index];
current.shiftAnimation.stop();
current.shift = current.finalShift = shift;
_layout->setHorizontalShift(index, shift);
checkForScrollAnimation();
const auto count = _entries.size();
const auto currentWidth = current.widget->width;
const auto currentMiddle = current.widget->left
+ shift
+ currentWidth / 2;
_currentDesiredIndex = index;
if (shift > 0) {
for (auto next = index + 1; next != count; ++next) {
if (isIndexPinned(next)) {
return;
}
const auto &e = _entries[next];
if (currentMiddle < e.widget->left + e.widget->width / 2) {
moveToShift(next, 0);
} else {
_currentDesiredIndex = next;
moveToShift(next, -currentWidth);
}
}
for (auto prev = index - 1; prev >= 0; --prev) {
moveToShift(prev, 0);
}
} else {
for (auto next = index + 1; next != count; ++next) {
moveToShift(next, 0);
}
for (auto prev = index - 1; prev >= 0; --prev) {
if (isIndexPinned(prev)) {
return;
}
const auto &e = _entries[prev];
if (currentMiddle >= e.widget->left + e.widget->width / 2) {
moveToShift(prev, 0);
} else {
_currentDesiredIndex = prev;
moveToShift(prev, currentWidth);
}
}
}
}
void ChatsFiltersTabsReorder::mousePress(
Qt::MouseButton button,
QPoint position,
QPoint globalPosition) {
if (button != Qt::LeftButton) {
return;
}
auto widget = (ChatsFiltersTabs::ShiftedSection*)(nullptr);
for (auto i = 0; i != _layout->_sections.size(); ++i) {
auto &section = _layout->_sections[i];
if ((position.x() >= section.section->left)
&& (position.x() < (section.section->left + section.section->width))) {
widget = &section;
break;
}
}
cancelCurrent();
if (!widget) {
return;
}
_currentWidget = widget->section;
_currentShiftedWidget = widget;
_currentStart = globalPosition.x();
}
void ChatsFiltersTabsReorder::mouseMove(QPoint position) {
if (!_currentWidget) {
// if (_currentWidget != widget) {
return;
} else if (_currentState != State::Started) {
checkForStart(position);
} else {
updateOrder(indexOf(_currentWidget), position);
}
}
void ChatsFiltersTabsReorder::mouseRelease(Qt::MouseButton button) {
if (button != Qt::LeftButton) {
return;
}
finishReordering();
}
void ChatsFiltersTabsReorder::cancelCurrent() {
if (_currentWidget) {
cancelCurrent(indexOf(_currentWidget));
}
}
void ChatsFiltersTabsReorder::cancelCurrent(int index) {
Expects(_currentWidget != nullptr);
if (_currentState == State::Started) {
_currentState = State::Cancelled;
_updates.fire({ _currentWidget, index, index, _currentState });
}
_currentWidget = nullptr;
_currentShiftedWidget = nullptr;
for (auto i = 0, count = int(_entries.size()); i != count; ++i) {
moveToShift(i, 0);
}
}
void ChatsFiltersTabsReorder::finishReordering() {
if (_scroll) {
_scrollAnimation.stop();
}
finishCurrent();
}
void ChatsFiltersTabsReorder::finishCurrent() {
if (!_currentWidget) {
return;
}
const auto index = indexOf(_currentWidget);
if (_currentDesiredIndex == index || _currentState != State::Started) {
cancelCurrent(index);
return;
}
const auto result = _currentDesiredIndex;
const auto widget = _currentWidget;
_currentState = State::Cancelled;
_currentWidget = nullptr;
_currentShiftedWidget = nullptr;
auto &current = _entries[index];
const auto width = current.widget->width;
if (index < result) {
auto sum = 0;
for (auto i = index; i != result; ++i) {
auto &entry = _entries[i + 1];
const auto widget = entry.widget;
entry.deltaShift += width;
updateShift(widget, i + 1);
sum += widget->width;
}
current.finalShift -= sum;
} else if (index > result) {
auto sum = 0;
for (auto i = result; i != index; ++i) {
auto &entry = _entries[i];
const auto widget = entry.widget;
entry.deltaShift -= width;
updateShift(widget, i);
sum += widget->width;
}
current.finalShift += sum;
}
if (!(current.finalShift + current.deltaShift)) {
current.shift = 0;
_layout->setHorizontalShift(index, 0);
}
base::reorder(_entries, index, result);
_layout->reorderSections(index, _currentDesiredIndex);
for (auto i = 0; i != _layout->sectionsRef().size(); ++i) {
_entries[i].widget = &_layout->sectionsRef()[i];
moveToShift(i, 0);
}
_updates.fire({ widget, index, result, State::Applied });
}
void ChatsFiltersTabsReorder::moveToShift(int index, int shift) {
auto &entry = _entries[index];
if (entry.finalShift + entry.deltaShift == shift) {
return;
}
const auto widget = entry.widget;
entry.shiftAnimation.start(
[=, this] { updateShift(widget, index); },
entry.finalShift,
shift - entry.deltaShift,
st::slideWrapDuration);
entry.finalShift = shift - entry.deltaShift;
}
void ChatsFiltersTabsReorder::updateShift(
not_null<Section*> widget,
int indexHint) {
Expects(indexHint >= 0 && indexHint < _entries.size());
const auto index = (_entries[indexHint].widget == widget)
? indexHint
: indexOf(widget);
auto &entry = _entries[index];
entry.shift = base::SafeRound(
entry.shiftAnimation.value(entry.finalShift)
) + entry.deltaShift;
if (entry.deltaShift && !entry.shiftAnimation.animating()) {
entry.finalShift += entry.deltaShift;
entry.deltaShift = 0;
}
_layout->setHorizontalShift(index, entry.shift);
}
int ChatsFiltersTabsReorder::indexOf(not_null<Section*> widget) const {
const auto i = ranges::find(_entries, widget, &Entry::widget);
Assert(i != end(_entries));
return i - begin(_entries);
}
auto ChatsFiltersTabsReorder::updates() const -> rpl::producer<Single> {
return _updates.events();
}
void ChatsFiltersTabsReorder::updateScrollCallback() {
if (!_scroll) {
return;
}
const auto delta = deltaFromEdge();
const auto oldLeft = _scroll->scrollLeft();
_scroll->horizontalScrollBar()->setValue(oldLeft + delta);
const auto newLeft = _scroll->scrollLeft();
_currentStart += oldLeft - newLeft;
if (newLeft == 0 || newLeft == _scroll->scrollLeftMax()) {
_scrollAnimation.stop();
}
}
void ChatsFiltersTabsReorder::checkForScrollAnimation() {
if (!_scroll || !deltaFromEdge() || _scrollAnimation.animating()) {
return;
}
_scrollAnimation.start();
}
int ChatsFiltersTabsReorder::deltaFromEdge() {
Expects(_currentWidget != nullptr);
Expects(_currentShiftedWidget != nullptr);
Expects(_scroll);
const auto globalPosition = _layout->mapToGlobal(
QPoint(
_currentWidget->left + _currentShiftedWidget->horizontalShift,
0));
const auto localLeft = _scroll->mapFromGlobal(globalPosition).x();
const auto localRight = localLeft
+ _currentWidget->width
- _scroll->width();
const auto isLeftEdge = (localLeft < 0);
const auto isRightEdge = (localRight > 0);
if (!isLeftEdge && !isRightEdge) {
_scrollAnimation.stop();
return 0;
}
return int((isRightEdge ? localRight : localLeft) * kScrollFactor);
}
} // namespace Ui

View File

@@ -0,0 +1,98 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
#include "ui/effects/animations.h"
#include "ui/widgets/chat_filters_tabs_slider.h"
namespace Ui {
class ScrollArea;
class ChatsFiltersTabsReorder final {
public:
using Section = ChatsFiltersTabs::Section;
enum class State : uchar {
Started,
Applied,
Cancelled,
};
struct Single {
not_null<Section*> widget;
int oldPosition = 0;
int newPosition = 0;
State state = State::Started;
};
ChatsFiltersTabsReorder(
not_null<ChatsFiltersTabs*> layout,
not_null<ScrollArea*> scroll);
ChatsFiltersTabsReorder(not_null<ChatsFiltersTabs*> layout);
void start();
void cancel();
void finishReordering();
void addPinnedInterval(int from, int length);
void clearPinnedIntervals();
[[nodiscard]] rpl::producer<Single> updates() const;
private:
struct Entry {
not_null<Section*> widget;
Ui::Animations::Simple shiftAnimation;
int shift = 0;
int finalShift = 0;
int deltaShift = 0;
};
struct Interval {
[[nodiscard]] bool isIn(int index) const;
int from = 0;
int length = 0;
};
void mousePress(Qt::MouseButton button, QPoint position, QPoint global);
void mouseMove(QPoint position);
void mouseRelease(Qt::MouseButton button);
void checkForStart(QPoint position);
void updateOrder(int index, QPoint position);
void cancelCurrent();
void finishCurrent();
void cancelCurrent(int index);
[[nodiscard]] int indexOf(not_null<Section*> widget) const;
void moveToShift(int index, int shift);
void updateShift(not_null<Section*> widget, int indexHint);
void updateScrollCallback();
void checkForScrollAnimation();
[[nodiscard]] int deltaFromEdge();
[[nodiscard]] bool isIndexPinned(int index) const;
const not_null<ChatsFiltersTabs*> _layout;
Ui::ScrollArea *_scroll = nullptr;
Ui::Animations::Basic _scrollAnimation;
std::vector<Interval> _pinnedIntervals;
Section *_currentWidget = nullptr;
ChatsFiltersTabs::ShiftedSection *_currentShiftedWidget = nullptr;
int _currentStart = 0;
int _currentDesiredIndex = 0;
State _currentState = State::Cancelled;
std::vector<Entry> _entries;
rpl::event_stream<Single> _updates;
rpl::lifetime _lifetime;
};
} // namespace Ui

View File

@@ -0,0 +1,494 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "ui/widgets/chat_filters_tabs_strip.h"
#include "api/api_chat_filters_remove_manager.h"
#include "boxes/filters/edit_filter_box.h"
#include "boxes/premium_limits_box.h"
#include "core/application.h"
#include "core/ui_integration.h"
#include "data/data_chat_filters.h"
#include "data/data_peer_values.h" // Data::AmPremiumValue.
#include "data/data_premium_limits.h"
#include "data/data_session.h"
#include "data/data_unread_value.h"
#include "data/data_user.h"
#include "lang/lang_keys.h"
#include "main/main_session.h"
#include "settings/settings_folders.h"
#include "ui/widgets/menu/menu_action.h"
#include "ui/power_saving.h"
#include "ui/ui_utility.h"
#include "ui/widgets/chat_filters_tabs_slider_reorder.h"
#include "ui/widgets/menu/menu_add_action_callback_factory.h"
#include "ui/widgets/popup_menu.h"
#include "ui/widgets/scroll_area.h"
#include "ui/wrap/slide_wrap.h"
#include "window/window_controller.h"
#include "window/window_peer_menu.h"
#include "window/window_session_controller.h"
#include "styles/style_dialogs.h" // dialogsSearchTabs
#include "styles/style_media_player.h" // mediaPlayerMenuCheck
#include "styles/style_menu_icons.h"
#include <QScrollBar>
namespace Ui {
namespace {
struct State final {
Ui::Animations::Simple animation;
std::optional<FilterId> lastFilterId = std::nullopt;
rpl::lifetime rebuildLifetime;
rpl::lifetime reorderLifetime;
base::unique_qptr<Ui::PopupMenu> menu;
Api::RemoveComplexChatFilter removeApi;
bool waitingSuggested = false;
std::unique_ptr<Ui::ChatsFiltersTabsReorder> reorder;
bool ignoreRefresh = false;
};
void ShowMenu(
not_null<Ui::RpWidget*> parent,
not_null<Window::SessionController*> controller,
not_null<State*> state,
int index) {
const auto session = &controller->session();
auto id = FilterId(0);
{
const auto &list = session->data().chatsFilters().list();
if (index < 0 || index >= list.size()) {
return;
}
id = list[index].id();
}
state->menu = base::make_unique_q<Ui::PopupMenu>(
parent,
st::popupMenuWithIcons);
const auto addAction = Ui::Menu::CreateAddActionCallback(
state->menu.get());
if (id) {
addAction(
tr::lng_filters_context_edit(tr::now),
[=] { EditExistingFilter(controller, id); },
&st::menuIconEdit);
Window::MenuAddMarkAsReadChatListAction(
controller,
[=] { return session->data().chatsFilters().chatsList(id); },
addAction);
auto showRemoveBox = [=] {
state->removeApi.request(base::make_weak(parent), controller, id);
};
addAction({
.text = tr::lng_filters_context_remove(tr::now),
.handler = std::move(showRemoveBox),
.icon = &st::menuIconDeleteAttention,
.isAttention = true,
});
} else {
auto customUnreadState = [=] {
return Data::MainListMapUnreadState(
session,
session->data().chatsList()->unreadState());
};
Window::MenuAddMarkAsReadChatListAction(
controller,
[=] { return session->data().chatsList(); },
addAction,
std::move(customUnreadState));
auto openFiltersSettings = [=] {
const auto filters = &session->data().chatsFilters();
if (filters->suggestedLoaded()) {
controller->showSettings(Settings::Folders::Id());
} else if (!state->waitingSuggested) {
state->waitingSuggested = true;
filters->requestSuggested();
filters->suggestedUpdated(
) | rpl::take(1) | rpl::on_next([=] {
controller->showSettings(Settings::Folders::Id());
}, parent->lifetime());
}
};
addAction(
tr::lng_filters_setup_menu(tr::now),
std::move(openFiltersSettings),
&st::menuIconEdit);
}
if (state->menu->empty()) {
state->menu = nullptr;
return;
}
state->menu->popup(QCursor::pos());
}
void ShowFiltersListMenu(
not_null<Ui::RpWidget*> parent,
not_null<Main::Session*> session,
not_null<State*> state,
int active,
Fn<void(int)> changeActive) {
const auto &list = session->data().chatsFilters().list();
state->menu = base::make_unique_q<Ui::PopupMenu>(
parent,
st::popupMenuWithIcons);
const auto reorderAll = session->user()->isPremium();
const auto maxLimit = (reorderAll ? 1 : 0)
+ Data::PremiumLimits(session).dialogFiltersCurrent();
const auto premiumFrom = (reorderAll ? 0 : 1) + maxLimit;
for (auto i = 0; i < list.size(); ++i) {
const auto title = list[i].title();
const auto text = title.text.empty()
? tr::lng_filters_all_short(tr::now)
: title.text.text;
const auto callback = [=] {
if (i != active) {
changeActive(i);
}
};
const auto icon = (i == active)
? &st::mediaPlayerMenuCheck
: nullptr;
const auto action = Ui::Menu::CreateAction(
state->menu.get(),
text,
callback);
auto item = base::make_unique_q<Ui::Menu::Action>(
state->menu.get(),
state->menu->st().menu,
action,
icon,
icon);
action->setEnabled(i < premiumFrom);
if (!title.text.empty()) {
const auto context = Core::TextContext({
.session = session,
.repaint = [raw = item.get()] { raw->update(); },
.customEmojiLoopLimit = title.isStatic ? -1 : 0,
});
item->setMarkedText(title.text, QString(), context);
}
state->menu->addAction(std::move(item));
}
session->data().chatsFilters().changed() | rpl::on_next([=] {
state->menu->hideMenu();
}, state->menu->lifetime());
if (state->menu->empty()) {
state->menu = nullptr;
return;
}
state->menu->popup(QCursor::pos());
}
} // namespace
not_null<Ui::RpWidget*> AddChatFiltersTabsStrip(
not_null<Ui::RpWidget*> parent,
not_null<Main::Session*> session,
Fn<void(FilterId)> choose,
ChatHelpers::PauseReason pauseLevel,
Window::SessionController *controller,
bool trackActiveFilterAndUnreadAndReorder) {
const auto wrap = Ui::CreateChild<Ui::SlideWrap<Ui::RpWidget>>(
parent,
object_ptr<Ui::RpWidget>(parent));
if (!controller) {
const auto window = Core::App().findWindow(parent);
controller = window ? window->sessionController() : nullptr;
if (!controller) {
return wrap;
}
}
const auto container = wrap->entity();
const auto scroll = Ui::CreateChild<Ui::ScrollArea>(
container,
st::dialogsTabsScroll,
true);
const auto slider = scroll->setOwnedWidget(
object_ptr<Ui::ChatsFiltersTabs>(
parent,
trackActiveFilterAndUnreadAndReorder
? st::dialogsSearchTabs
: st::chatsFiltersTabs));
const auto state = wrap->lifetime().make_state<State>();
const auto reassignUnreadValue = [=] {
state->reorderLifetime.destroy();
const auto &list = session->data().chatsFilters().list();
auto includeMuted = Data::IncludeMutedCounterFoldersValue();
for (auto i = 0; i < list.size(); i++) {
rpl::combine(
Data::UnreadStateValue(session, list[i].id()),
rpl::duplicate(includeMuted)
) | rpl::on_next([=](
const Dialogs::UnreadState &state,
bool includeMuted) {
const auto chats = state.chats;
const auto chatsMuted = state.chatsMuted;
const auto muted = (chatsMuted + state.marksMuted);
const auto count = (chats + state.marks)
- (includeMuted ? 0 : muted);
const auto isMuted = includeMuted && (count == muted);
slider->setUnreadCount(i, count, isMuted);
slider->fitWidthToSections();
}, state->reorderLifetime);
}
};
if (trackActiveFilterAndUnreadAndReorder) {
using Reorder = Ui::ChatsFiltersTabsReorder;
state->reorder = std::make_unique<Reorder>(slider, scroll);
const auto applyReorder = [=](
int oldPosition,
int newPosition) {
if (newPosition == oldPosition) {
return;
}
const auto filters = &session->data().chatsFilters();
const auto &list = filters->list();
if (!session->user()->isPremium()) {
if (list[0].id() != FilterId()) {
filters->moveAllToFront();
}
}
Assert(oldPosition >= 0 && oldPosition < list.size());
Assert(newPosition >= 0 && newPosition < list.size());
auto order = ranges::views::all(
list
) | ranges::views::transform(
&Data::ChatFilter::id
) | ranges::to_vector;
base::reorder(order, oldPosition, newPosition);
state->ignoreRefresh = true;
filters->saveOrder(order);
state->ignoreRefresh = false;
};
state->reorder->updates(
) | rpl::on_next([=](const Reorder::Single &data) {
if (data.state == Reorder::State::Started) {
slider->setReordering(slider->reordering() + 1);
} else {
Ui::PostponeCall(slider, [=] {
slider->setReordering(slider->reordering() - 1);
});
if (data.state == Reorder::State::Applied) {
applyReorder(data.oldPosition, data.newPosition);
reassignUnreadValue();
}
}
}, slider->lifetime());
}
wrap->toggle(false, anim::type::instant);
scroll->setCustomWheelProcess([=](not_null<QWheelEvent*> e) {
const auto pixelDelta = e->pixelDelta();
const auto angleDelta = e->angleDelta();
if (std::abs(pixelDelta.x()) + std::abs(angleDelta.x())) {
return false;
}
const auto bar = scroll->horizontalScrollBar();
const auto y = pixelDelta.y() ? pixelDelta.y() : angleDelta.y();
bar->setValue(bar->value() - y);
return true;
});
const auto scrollToIndex = [=](int index, anim::type type) {
const auto to = index
? (slider->centerOfSection(index) - scroll->width() / 2)
: 0;
const auto bar = scroll->horizontalScrollBar();
state->animation.stop();
if (type == anim::type::instant) {
bar->setValue(to);
} else {
state->animation.start(
[=](float64 v) { bar->setValue(v); },
bar->value(),
std::min(to, bar->maximum()),
st::defaultTabsSlider.duration);
}
};
const auto applyFilter = [=](const Data::ChatFilter &filter) {
if (slider->reordering()) {
return;
}
choose(filter.id());
};
const auto filterByIndex = [=](int index) -> const Data::ChatFilter& {
const auto &list = session->data().chatsFilters().list();
Assert(index >= 0 && index < list.size());
return list[index];
};
const auto rebuild = [=] {
const auto &list = session->data().chatsFilters().list();
if ((list.size() <= 1 && !slider->width()) || state->ignoreRefresh) {
return;
}
const auto context = Core::TextContext({ .session = session });
const auto paused = [=] {
return On(PowerSaving::kEmojiChat)
|| controller->isGifPausedAtLeastFor(pauseLevel);
};
const auto sectionsChanged = slider->setSectionsAndCheckChanged(
ranges::views::all(
list
) | ranges::views::transform([](const Data::ChatFilter &filter) {
auto title = filter.title();
return title.text.empty()
? TextWithEntities{ tr::lng_filters_all_short(tr::now) }
: title.isStatic
? Data::ForceCustomEmojiStatic(title.text)
: title.text;
}) | ranges::to_vector, context, paused);
if (!sectionsChanged) {
return;
}
state->rebuildLifetime.destroy();
slider->fitWidthToSections();
{
const auto reorderAll = session->user()->isPremium();
const auto maxLimit = (reorderAll ? 1 : 0)
+ Data::PremiumLimits(session).dialogFiltersCurrent();
const auto premiumFrom = (reorderAll ? 0 : 1) + maxLimit;
slider->setLockedFrom((premiumFrom >= list.size())
? 0
: premiumFrom);
slider->lockedClicked() | rpl::on_next([=] {
controller->show(Box(FiltersLimitBox, session, std::nullopt));
}, state->rebuildLifetime);
if (state->reorder) {
state->reorder->cancel();
state->reorder->clearPinnedIntervals();
if (!reorderAll) {
state->reorder->addPinnedInterval(0, 1);
}
state->reorder->addPinnedInterval(
premiumFrom,
std::max(1, int(list.size()) - maxLimit));
}
}
if (trackActiveFilterAndUnreadAndReorder) {
reassignUnreadValue();
}
[&] {
const auto lookingId = state->lastFilterId.value_or(list[0].id());
for (auto i = 0; i < list.size(); i++) {
const auto &filter = list[i];
if (filter.id() == lookingId) {
const auto wasLast = !!state->lastFilterId;
state->lastFilterId = filter.id();
slider->setActiveSectionFast(i);
scrollToIndex(
i,
wasLast ? anim::type::normal : anim::type::instant);
applyFilter(filter);
return;
}
}
if (list.size()) {
const auto index = 0;
const auto &filter = filterByIndex(index);
state->lastFilterId = filter.id();
slider->setActiveSectionFast(index);
scrollToIndex(index, anim::type::instant);
applyFilter(filter);
}
}();
if (trackActiveFilterAndUnreadAndReorder) {
controller->activeChatsFilter(
) | rpl::on_next([=](FilterId id) {
const auto &list = session->data().chatsFilters().list();
for (auto i = 0; i < list.size(); ++i) {
if (list[i].id() == id) {
slider->setActiveSection(i);
scrollToIndex(i, anim::type::normal);
break;
}
}
state->reorder->finishReordering();
}, state->rebuildLifetime);
}
rpl::single(-1) | rpl::then(
slider->sectionActivated()
) | rpl::combine_previous(
) | rpl::on_next([=](int was, int index) {
if (slider->reordering()) {
return;
}
const auto &filter = filterByIndex(index);
if (was != index) {
state->lastFilterId = filter.id();
scrollToIndex(index, anim::type::normal);
}
applyFilter(filter);
}, state->rebuildLifetime);
slider->contextMenuRequested() | rpl::on_next([=](int index) {
if (trackActiveFilterAndUnreadAndReorder) {
ShowMenu(wrap, controller, state, index);
} else {
ShowFiltersListMenu(
wrap,
session,
state,
slider->activeSection(),
[=](int i) { slider->setActiveSection(i); });
}
}, state->rebuildLifetime);
wrap->toggle((list.size() > 1), anim::type::instant);
if (state->reorder) {
state->reorder->start();
}
};
rpl::combine(
session->data().chatsFilters().changed(),
Data::AmPremiumValue(session) | rpl::to_empty
) | rpl::on_next(rebuild, wrap->lifetime());
rebuild();
session->data().chatsFilters().isChatlistChanged(
) | rpl::on_next([=](FilterId id) {
if (!id || !state->lastFilterId || (id != state->lastFilterId)) {
return;
}
for (const auto &filter : session->data().chatsFilters().list()) {
if (filter.id() == id) {
applyFilter(filter);
return;
}
}
}, wrap->lifetime());
rpl::combine(
parent->widthValue() | rpl::filter(rpl::mappers::_1 > 0),
slider->heightValue() | rpl::filter(rpl::mappers::_1 > 0)
) | rpl::on_next([=](int w, int h) {
scroll->resize(w, h);
container->resize(w, h);
wrap->resize(w, h);
}, wrap->lifetime());
return wrap;
}
} // namespace Ui

View File

@@ -0,0 +1,36 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
namespace ChatHelpers {
enum class PauseReason;
} // namespace ChatHelpers
namespace Main {
class Session;
} // namespace Main
namespace Ui {
class RpWidget;
} // namespace Ui
namespace Window {
class SessionController;
} // namespace Window
namespace Ui {
not_null<Ui::RpWidget*> AddChatFiltersTabsStrip(
not_null<Ui::RpWidget*> parent,
not_null<Main::Session*> session,
Fn<void(FilterId)> choose,
ChatHelpers::PauseReason pauseLevel,
Window::SessionController *controller = nullptr,
bool trackActiveFilterAndUnreadAndReorder = false);
} // namespace Ui

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,106 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
#include "ui/rp_widget.h"
#include "base/object_ptr.h"
class ColorEditor : public Ui::RpWidget {
public:
enum class Mode {
RGBA,
HSL,
};
ColorEditor(
QWidget *parent,
Mode mode,
QColor current);
void setLightnessLimits(int min, int max);
[[nodiscard]] QColor color() const;
[[nodiscard]] rpl::producer<QColor> colorValue() const;
[[nodiscard]] rpl::producer<> submitRequests() const;
void showColor(QColor color);
void setCurrent(QColor color);
void setInnerFocus() const;
protected:
void prepare();
void paintEvent(QPaintEvent *e) override;
void resizeEvent(QResizeEvent *e) override;
void mousePressEvent(QMouseEvent *e) override;
private:
struct HSB { // HSV or HSL depending on Mode.
int hue = 0;
int saturation = 0;
int brightness = 0;
};
void fieldSubmitted();
[[nodiscard]] HSB hsbFromControls() const;
void updateFromColor(QColor color);
void updateControlsFromColor();
void updateControlsFromHSB(HSB hsb);
void updateHSBFields();
void updateRGBFields();
void updateResultField();
void updateFromControls();
void updateFromHSBFields();
void updateFromRGBFields();
void updateFromResultField();
void setHSB(HSB hsb, int alpha);
void setRGB(int red, int green, int blue, int alpha);
[[nodiscard]] QColor applyLimits(QColor color) const;
int percentFromByte(int byte) {
return std::clamp(qRound(byte * 100 / 255.), 0, 100);
}
int percentToByte(int percent) {
return std::clamp(qRound(percent * 255 / 100.), 0, 255);
}
class Picker;
class Slider;
class Field;
class ResultField;
Mode _mode = Mode();
object_ptr<Picker> _picker;
object_ptr<Slider> _hueSlider = { nullptr };
object_ptr<Slider> _opacitySlider = { nullptr };
object_ptr<Slider> _lightnessSlider = { nullptr };
object_ptr<Field> _hueField;
object_ptr<Field> _saturationField;
object_ptr<Field> _brightnessField;
object_ptr<Field> _redField;
object_ptr<Field> _greenField;
object_ptr<Field> _blueField;
object_ptr<ResultField> _result;
QBrush _transparent;
QColor _current;
QColor _new;
QRect _currentRect;
QRect _newRect;
int _lightnessMin = 0;
int _lightnessMax = 255;
rpl::event_stream<> _submitRequests;
rpl::event_stream<QColor> _newChanges;
};

View File

@@ -0,0 +1,498 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "ui/widgets/continuous_sliders.h"
#include "ui/painter.h"
#include "ui/rect.h"
#include "base/timer.h"
#include "base/platform/base_platform_info.h"
#include "styles/style_widgets.h"
namespace Ui {
namespace {
constexpr auto kByWheelFinishedTimeout = 1000;
} // namespace
ContinuousSlider::ContinuousSlider(QWidget *parent) : RpWidget(parent) {
setCursor(style::cur_pointer);
}
void ContinuousSlider::setDisabled(bool disabled) {
if (_disabled != disabled) {
_disabled = disabled;
setCursor(_disabled ? style::cur_default : style::cur_pointer);
update();
}
}
void ContinuousSlider::setMoveByWheel(bool move) {
if (move != moveByWheel()) {
if (move) {
_byWheelFinished = std::make_unique<base::Timer>([=] {
if (_changeFinishedCallback) {
_changeFinishedCallback(getCurrentValue());
}
});
} else {
_byWheelFinished = nullptr;
}
}
}
QRect ContinuousSlider::getSeekRect() const {
const auto decrease = getSeekDecreaseSize();
return isHorizontal()
? QRect(decrease.width() / 2, 0, width() - decrease.width(), height())
: QRect(0, decrease.height() / 2, width(), height() - decrease.width());
}
float64 ContinuousSlider::value() const {
return getCurrentValue();
}
void ContinuousSlider::setValue(float64 value) {
setValue(value, -1);
}
void ContinuousSlider::setValue(float64 value, float64 receivedTill) {
if (_value != value || _receivedTill != receivedTill) {
_value = value;
_receivedTill = receivedTill;
update();
}
}
void ContinuousSlider::setFadeOpacity(float64 opacity) {
_fadeOpacity = opacity;
update();
}
void ContinuousSlider::mouseMoveEvent(QMouseEvent *e) {
if (_mouseDown) {
updateDownValueFromPos(e->pos());
}
}
float64 ContinuousSlider::computeValue(const QPoint &pos) const {
const auto seekRect = myrtlrect(getSeekRect());
const auto result = isHorizontal() ?
(pos.x() - seekRect.x()) / float64(seekRect.width()) :
(1. - (pos.y() - seekRect.y()) / float64(seekRect.height()));
const auto snapped = std::clamp(result, 0., 1.);
return _adjustCallback ? _adjustCallback(snapped) : snapped;
}
void ContinuousSlider::mousePressEvent(QMouseEvent *e) {
_mouseDown = true;
_downValue = computeValue(e->pos());
update();
if (_changeProgressCallback) {
_changeProgressCallback(_downValue);
}
}
void ContinuousSlider::mouseReleaseEvent(QMouseEvent *e) {
if (_mouseDown) {
_mouseDown = false;
if (_changeFinishedCallback) {
_changeFinishedCallback(_downValue);
}
_value = _downValue;
update();
}
}
void ContinuousSlider::wheelEvent(QWheelEvent *e) {
if (_mouseDown || !moveByWheel()) {
return;
}
constexpr auto step = static_cast<int>(QWheelEvent::DefaultDeltasPerStep);
constexpr auto coef = 1. / (step * 10.);
auto deltaX = e->angleDelta().x(), deltaY = e->angleDelta().y();
if (Platform::IsMac()) {
deltaY *= -1;
} else {
deltaX *= -1;
}
auto delta = (qAbs(deltaX) > qAbs(deltaY)) ? deltaX : deltaY;
auto finalValue = std::clamp(_value + delta * coef, 0., 1.);
setValue(finalValue);
if (_changeProgressCallback) {
_changeProgressCallback(finalValue);
}
_byWheelFinished->callOnce(kByWheelFinishedTimeout);
}
void ContinuousSlider::keyPressEvent(QKeyEvent *e) {
const auto changeBy = [&](float64 step) {
Expects(step != 0.);
auto steps = 0;
while (true) {
++steps;
auto result = _value + (steps * step);
const auto stopping = (result <= 0.) || (result >= 1.);
if (_adjustCallback) {
result = _adjustCallback(result);
}
result = std::clamp(result, 0., 1.);
if (result != _value || stopping) {
return result;
}
}
};
const auto newValue = [&] {
constexpr auto kSmallStep = 0.01;
constexpr auto kLargeStep = 0.10;
switch (e->key()) {
case Qt::Key_Right:
case Qt::Key_Up: return changeBy(kSmallStep);
case Qt::Key_Left:
case Qt::Key_Down: return changeBy(-kSmallStep);
case Qt::Key_PageUp: return changeBy(kLargeStep);
case Qt::Key_PageDown: return changeBy(-kLargeStep);
case Qt::Key_Home: return changeBy(-1.);
case Qt::Key_End: return changeBy(1.);
default: e->ignore();
}
return _value;
}();
if (_value == newValue) {
return;
}
setValue(newValue);
if (_changeProgressCallback) {
_changeProgressCallback(_value);
}
if (_changeFinishedCallback) {
_changeFinishedCallback(_value);
}
accessibilityValueChanged();
}
void ContinuousSlider::updateDownValueFromPos(const QPoint &pos) {
_downValue = computeValue(pos);
update();
if (_changeProgressCallback) {
_changeProgressCallback(_downValue);
}
}
void ContinuousSlider::enterEventHook(QEnterEvent *e) {
setOver(true);
}
void ContinuousSlider::leaveEventHook(QEvent *e) {
setOver(false);
}
void ContinuousSlider::setOver(bool over) {
if (_over == over) return;
_over = over;
auto from = _over ? 0. : 1., to = _over ? 1. : 0.;
_overAnimation.start([=] { update(); }, from, to, getOverDuration());
}
FilledSlider::FilledSlider(QWidget *parent, const style::FilledSlider &st) : ContinuousSlider(parent)
, _st(st) {
}
QSize FilledSlider::getSeekDecreaseSize() const {
return QSize(0, 0);
}
float64 FilledSlider::getOverDuration() const {
return _st.duration;
}
void FilledSlider::paintEvent(QPaintEvent *e) {
auto p = QPainter(this);
PainterHighQualityEnabler hq(p);
p.setPen(Qt::NoPen);
const auto masterOpacity = fadeOpacity();
const auto disabled = isDisabled();
const auto over = getCurrentOverFactor();
const auto lineWidth = _st.lineWidth + ((_st.fullWidth - _st.lineWidth) * over);
const auto lineWidthRounded = std::floor(lineWidth);
const auto lineWidthPartial = lineWidth - lineWidthRounded;
const auto seekRect = getSeekRect();
const auto value = getCurrentValue();
const auto from = seekRect.x();
const auto mid = qRound(from + value * seekRect.width());
const auto end = from + seekRect.width();
if (mid > from) {
p.setOpacity(masterOpacity);
p.fillRect(from, height() - lineWidthRounded, (mid - from), lineWidthRounded, disabled ? _st.disabledFg : _st.activeFg);
if (lineWidthPartial > 0.01) {
p.setOpacity(masterOpacity * lineWidthPartial);
p.fillRect(from, height() - lineWidthRounded - 1, (mid - from), 1, disabled ? _st.disabledFg : _st.activeFg);
}
}
if (end > mid && over > 0) {
p.setOpacity(masterOpacity * over);
p.fillRect(mid, height() - lineWidthRounded, (end - mid), lineWidthRounded, _st.inactiveFg);
if (lineWidthPartial > 0.01) {
p.setOpacity(masterOpacity * over * lineWidthPartial);
p.fillRect(mid, height() - lineWidthRounded - 1, (end - mid), 1, _st.inactiveFg);
}
}
}
MediaSlider::MediaSlider(QWidget *parent, const style::MediaSlider &st) : ContinuousSlider(parent)
, _st(st) {
}
QSize MediaSlider::getSeekDecreaseSize() const {
return _alwaysDisplayMarker ? _st.seekSize : QSize();
}
float64 MediaSlider::getOverDuration() const {
return _st.duration;
}
void MediaSlider::disablePaint(bool disabled) {
_paintDisabled = disabled;
}
void MediaSlider::addDivider(float64 atValue, const QSize &size) {
_dividers.push_back(Divider{ atValue, size });
}
void MediaSlider::setColorOverrides(ColorOverrides overrides) {
_overrides = std::move(overrides);
update();
}
void MediaSlider::paintEvent(QPaintEvent *e) {
if (_paintDisabled) {
return;
}
auto p = QPainter(this);
PainterHighQualityEnabler hq(p);
p.setPen(Qt::NoPen);
p.setOpacity(fadeOpacity());
const auto horizontal = isHorizontal();
const auto borderWidth = _st.borderWidth;
const auto borderHalf = borderWidth / 2;
const auto radius = _st.width / 2;
const auto disabled = isDisabled();
const auto over = getCurrentOverFactor();
const auto seekRect = getSeekRect();
// invert colors and value for vertical
const auto value = horizontal
? getCurrentValue()
: (1. - getCurrentValue());
// receivedTill is not supported for vertical
const auto receivedTill = horizontal
? getCurrentReceivedTill()
: value;
const auto markerFrom = (horizontal ? seekRect.x() : seekRect.y());
const auto markerLength = horizontal
? seekRect.width()
: seekRect.height();
const auto from = 0;
const auto length = (horizontal ? width() : height());
const auto alwaysSeekSize = horizontal
? _st.seekSize.width()
: _st.seekSize.height();
const auto mid = _alwaysDisplayMarker
? qRound(from
+ (alwaysSeekSize / 2.)
+ value * (length - alwaysSeekSize))
: qRound(from + value * length);
const auto till = horizontal
? std::max(mid, qRound(from + receivedTill * length))
: mid;
const auto end = from + length;
const auto activeFg = disabled
? _st.activeFgDisabled
: _overrides.activeFg
? QBrush(*_overrides.activeFg)
: anim::brush(_st.activeFg, _st.activeFgOver, over);
const auto receivedTillFg = _st.receivedTillFg;
const auto inactiveFg = disabled
? _st.inactiveFgDisabled
: _overrides.inactiveFg
? QBrush(*_overrides.inactiveFg)
: anim::brush(_st.inactiveFg, _st.inactiveFgOver, over);
const auto borderFg = _st.borderFg;
if (mid > from) {
const auto fromClipRect = horizontal
? QRect(0, 0, mid, height())
: QRect(0, 0, width(), mid);
const auto till = std::min(mid + radius, end);
const auto fromRect = horizontal
? QRect(
from + borderHalf,
(height() - _st.width) / 2 + borderHalf,
till - from - borderWidth,
_st.width - borderWidth)
: QRect(
(width() - _st.width) / 2 + borderHalf,
from + borderHalf,
_st.width - borderWidth,
till - from - borderWidth);
p.setClipRect(fromClipRect);
if (borderWidth > 0) {
const auto borderPen = _overrides.activeBorder
? QPen(*_overrides.activeBorder, borderWidth)
: QPen(borderFg, borderWidth);
const auto bgBrush = _overrides.activeBg
? QBrush(*_overrides.activeBg)
: (horizontal ? borderFg : inactiveFg);
p.setPen(borderPen);
p.setBrush(bgBrush);
} else {
p.setPen(Qt::NoPen);
p.setBrush(horizontal ? activeFg : inactiveFg);
}
p.drawRoundedRect(fromRect, radius, radius);
}
if (till > mid) {
Assert(horizontal);
auto clipRect = QRect(mid, 0, till - mid, height());
const auto left = std::max(mid - radius, from);
const auto right = std::min(till + radius, end);
const auto rect = QRect(
left,
(height() - _st.width) / 2,
right - left,
_st.width);
p.setClipRect(clipRect);
p.setBrush(receivedTillFg);
p.drawRoundedRect(rect, radius, radius);
}
if (end > till) {
const auto endClipRect = horizontal
? QRect(till, 0, width() - till, height())
: QRect(0, till, width(), height() - till);
const auto begin = std::max(till - radius, from);
const auto endRect = horizontal
? QRect(
begin + borderHalf,
(height() - _st.width) / 2 + borderHalf,
end - begin - borderWidth,
_st.width - borderWidth)
: QRect(
(width() - _st.width) / 2 + borderHalf,
begin + borderHalf,
_st.width - borderWidth,
end - begin - borderWidth);
p.setClipRect(endClipRect);
if (borderWidth > 0) {
const auto endBorderPen = _overrides.inactiveBorder
? QPen(*_overrides.inactiveBorder, borderWidth)
: QPen(borderFg, borderWidth);
p.setPen(endBorderPen);
} else {
p.setPen(Qt::NoPen);
}
p.setBrush(horizontal ? inactiveFg : activeFg);
p.drawRoundedRect(endRect, radius, radius);
}
if (!_dividers.empty()) {
p.setClipRect(rect());
for (const auto &divider : _dividers) {
const auto dividerValue = horizontal
? divider.atValue
: (1. - divider.atValue);
const auto dividerMid = base::SafeRound(from
+ dividerValue * length);
const auto &size = divider.size;
const auto rect = horizontal
? QRect(
dividerMid - size.width() / 2,
(height() - size.height()) / 2,
size.width(),
size.height())
: QRect(
(width() - size.height()) / 2,
dividerMid - size.width() / 2,
size.height(),
size.width());
p.setBrush(((value < dividerValue) == horizontal)
? inactiveFg
: activeFg);
const auto dividerRadius = size.width() / 2.;
p.drawRoundedRect(rect, dividerRadius, dividerRadius);
}
}
const auto markerSizeRatio = disabled
? 0.
: (_alwaysDisplayMarker ? 1. : over);
if (markerSizeRatio > 0) {
const auto position = qRound(markerFrom + value * markerLength)
- (horizontal
? (_st.seekSize.width() / 2)
: (_st.seekSize.height() / 2));
const auto seekButton = horizontal
? QRect(
position,
(height() - _st.seekSize.height()) / 2,
_st.seekSize.width(),
_st.seekSize.height())
: QRect(
(width() - _st.seekSize.width()) / 2,
position,
_st.seekSize.width(),
_st.seekSize.height());
const auto size = horizontal
? _st.seekSize.width()
: _st.seekSize.height();
const auto remove = static_cast<int>(
((1. - markerSizeRatio) * size) / 2.);
if (remove * 2 < size) {
p.setClipRect(rect());
const auto seekFg = _overrides.seekFg
? QBrush(*_overrides.seekFg)
: activeFg;
if (borderWidth > 0) {
const auto seekBorderPen = _overrides.seekBorder
? QPen(*_overrides.seekBorder, borderWidth)
: QPen(borderFg, borderWidth);
p.setPen(seekBorderPen);
p.setBrush(seekFg);
} else {
p.setPen(Qt::NoPen);
p.setBrush(seekFg);
}
const auto xshift = horizontal
? std::max(
seekButton.x() + seekButton.width() - remove - width(),
0) + std::min(seekButton.x() + remove, 0)
: 0;
const auto yshift = horizontal
? 0
: std::max(
seekButton.y() + seekButton.height() - remove - height(),
0) + std::min(seekButton.y() + remove, 0);
auto ellipseRect = (seekButton - Margins(remove)).translated(
-xshift,
-yshift);
if (borderWidth > 0) {
ellipseRect -= Margins(borderHalf);
}
p.drawEllipse(ellipseRect);
}
}
}
} // namespace Ui

View File

@@ -0,0 +1,274 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
#include "ui/effects/animations.h"
#include "ui/rp_widget.h"
namespace base {
class Timer;
} // namespace base
namespace style {
struct FilledSlider;
struct MediaSlider;
} // namespace style
namespace Ui {
class ContinuousSlider : public RpWidget {
public:
ContinuousSlider(QWidget *parent);
enum class Direction {
Horizontal,
Vertical,
};
void setDirection(Direction direction) {
_direction = direction;
update();
}
float64 value() const;
void setValue(float64 value);
void setValue(float64 value, float64 receivedTill);
void setFadeOpacity(float64 opacity);
void setDisabled(bool disabled);
bool isDisabled() const {
return _disabled;
}
void setAdjustCallback(Fn<float64(float64)> callback) {
_adjustCallback = std::move(callback);
}
void setChangeProgressCallback(Fn<void(float64)> callback) {
_changeProgressCallback = std::move(callback);
}
void setChangeFinishedCallback(Fn<void(float64)> callback) {
_changeFinishedCallback = std::move(callback);
}
bool isChanging() const {
return _mouseDown;
}
void setMoveByWheel(bool move);
QAccessible::Role accessibilityRole() override {
return QAccessible::Role::Slider;
}
QString accessibilityValue() const override {
const auto percent = std::clamp(qRound(_value * 100.), 0, 100);
return QString::number(percent) + '%';
}
protected:
void mouseMoveEvent(QMouseEvent *e) override;
void mousePressEvent(QMouseEvent *e) override;
void mouseReleaseEvent(QMouseEvent *e) override;
void wheelEvent(QWheelEvent *e) override;
void enterEventHook(QEnterEvent *e) override;
void leaveEventHook(QEvent *e) override;
void keyPressEvent(QKeyEvent *e) override;
float64 fadeOpacity() const {
return _fadeOpacity;
}
float64 getCurrentValue() const {
return _mouseDown ? _downValue : _value;
}
float64 getCurrentReceivedTill() const {
return _receivedTill;
}
float64 getCurrentOverFactor() {
return _disabled ? 0. : _overAnimation.value(_over ? 1. : 0.);
}
Direction getDirection() const {
return _direction;
}
bool isHorizontal() const {
return (_direction == Direction::Horizontal);
}
QRect getSeekRect() const;
virtual QSize getSeekDecreaseSize() const = 0;
private:
virtual float64 getOverDuration() const = 0;
bool moveByWheel() const {
return _byWheelFinished != nullptr;
}
void setOver(bool over);
float64 computeValue(const QPoint &pos) const;
void updateDownValueFromPos(const QPoint &pos);
Direction _direction = Direction::Horizontal;
bool _disabled = false;
std::unique_ptr<base::Timer> _byWheelFinished;
Fn<float64(float64)> _adjustCallback;
Fn<void(float64)> _changeProgressCallback;
Fn<void(float64)> _changeFinishedCallback;
bool _over = false;
Ui::Animations::Simple _overAnimation;
float64 _value = 0.;
float64 _receivedTill = 0.;
bool _mouseDown = false;
float64 _downValue = 0.;
float64 _fadeOpacity = 1.;
};
class FilledSlider : public ContinuousSlider {
public:
FilledSlider(QWidget *parent, const style::FilledSlider &st);
protected:
void paintEvent(QPaintEvent *e) override;
private:
QSize getSeekDecreaseSize() const override;
float64 getOverDuration() const override;
const style::FilledSlider &_st;
};
class MediaSlider : public ContinuousSlider {
public:
struct ColorOverrides {
std::optional<QColor> activeFg;
std::optional<QColor> activeBg;
std::optional<QColor> activeBorder;
std::optional<QColor> seekFg;
std::optional<QColor> seekBorder;
std::optional<QColor> inactiveFg;
std::optional<QColor> inactiveBorder;
};
MediaSlider(QWidget *parent, const style::MediaSlider &st);
void setAlwaysDisplayMarker(bool alwaysDisplayMarker) {
_alwaysDisplayMarker = alwaysDisplayMarker;
update();
}
void disablePaint(bool disabled);
template <
typename Value,
typename Convert,
typename Progress,
typename = std::enable_if_t<
rpl::details::is_callable_plain_v<Progress, Value>
&& std::is_same_v<Value, decltype(std::declval<Convert>()(1))>>>
void setPseudoDiscrete(
int valuesCount,
Convert &&convert,
Value current,
Progress &&progress,
int indexMin = 0) {
Expects(valuesCount > 1);
setAlwaysDisplayMarker(true);
setDirection(Ui::ContinuousSlider::Direction::Horizontal);
const auto sectionsCount = (valuesCount - 1);
setValue(1.);
for (auto index = index_type(); index != valuesCount; ++index) {
if (current <= convert(index)) {
setValue(index / float64(sectionsCount));
break;
}
}
setAdjustCallback([=](float64 value) {
return std::max(
base::SafeRound(value * sectionsCount),
indexMin * 1.
) / sectionsCount;
});
setChangeProgressCallback([=](float64 value) {
const auto index = std::max(
int(base::SafeRound(value * sectionsCount)),
indexMin);
progress(convert(index));
});
}
template <
typename Value,
typename Convert,
typename Progress,
typename Finished,
typename = std::enable_if_t<
rpl::details::is_callable_plain_v<Progress, Value>
&& rpl::details::is_callable_plain_v<Finished, Value>
&& std::is_same_v<Value, decltype(std::declval<Convert>()(1))>>>
void setPseudoDiscrete(
int valuesCount,
Convert &&convert,
Value current,
Progress &&progress,
Finished &&finished,
int indexMin = 0) {
setPseudoDiscrete(
valuesCount,
std::forward<Convert>(convert),
current,
std::forward<Progress>(progress),
indexMin);
setChangeFinishedCallback([=](float64 value) {
const auto sectionsCount = (valuesCount - 1);
const auto index = std::max(
int(base::SafeRound(value * sectionsCount)),
indexMin);
finished(convert(index));
});
}
void setColorOverrides(ColorOverrides overrides);
void addDivider(float64 atValue, const QSize &size);
protected:
void paintEvent(QPaintEvent *e) override;
private:
struct Divider {
const float64 atValue;
const QSize size;
};
QSize getSeekDecreaseSize() const override;
float64 getOverDuration() const override;
const style::MediaSlider &_st;
bool _alwaysDisplayMarker = false;
bool _paintDisabled = false;
std::vector<Divider> _dividers;
ColorOverrides _overrides;
};
class MediaSliderWheelless : public MediaSlider {
public:
using Ui::MediaSlider::MediaSlider;
protected:
void wheelEvent(QWheelEvent *e) override {
e->ignore();
}
};
} // namespace Ui

View File

@@ -0,0 +1,507 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "ui/widgets/discrete_sliders.h"
#include "ui/effects/ripple_animation.h"
#include "styles/style_widgets.h"
namespace Ui {
DiscreteSlider::DiscreteSlider(QWidget *parent, bool snapToLabel)
: RpWidget(parent)
, _snapToLabel(snapToLabel) {
setCursor(style::cur_pointer);
}
DiscreteSlider::~DiscreteSlider() = default;
void DiscreteSlider::setActiveSection(int index) {
_activeIndex = index;
activateCallback();
setSelectedSection(index);
}
void DiscreteSlider::activateCallback() {
if (_timerId >= 0) {
killTimer(_timerId);
_timerId = -1;
}
auto ms = crl::now();
if (ms >= _callbackAfterMs) {
_sectionActivated.fire_copy(_activeIndex);
} else {
_timerId = startTimer(_callbackAfterMs - ms, Qt::PreciseTimer);
}
}
void DiscreteSlider::timerEvent(QTimerEvent *e) {
activateCallback();
}
void DiscreteSlider::setActiveSectionFast(int index) {
setActiveSection(index);
finishAnimating();
}
void DiscreteSlider::finishAnimating() {
_a_left.stop();
_a_width.stop();
update();
_callbackAfterMs = 0;
if (_timerId >= 0) {
activateCallback();
}
}
void DiscreteSlider::setAdditionalContentWidthToSection(int index, int w) {
if (index >= 0 && index < _sections.size()) {
auto &section = _sections[index];
section.contentWidth = section.label.maxWidth() + w;
}
}
int DiscreteSlider::sectionsCount() const {
return int(_sections.size());
}
int DiscreteSlider::lookupSectionLeft(int index) const {
Expects(index >= 0 && index < _sections.size());
return _sections[index].left;
}
void DiscreteSlider::setSelectOnPress(bool selectOnPress) {
_selectOnPress = selectOnPress;
}
bool DiscreteSlider::paused() const {
return _paused && _paused();
}
std::vector<DiscreteSlider::Section> &DiscreteSlider::sectionsRef() {
return _sections;
}
void DiscreteSlider::addSection(const QString &label) {
_sections.push_back(Section(label, getLabelStyle()));
resizeToWidth(width());
}
void DiscreteSlider::addSection(
const TextWithEntities &label,
Text::MarkedContext context) {
context.repaint = [this] { update(); };
_sections.push_back(Section(label, getLabelStyle(), context));
resizeToWidth(width());
}
void DiscreteSlider::setSections(const std::vector<QString> &labels) {
Assert(!labels.empty());
_sections.clear();
for (const auto &label : labels) {
_sections.push_back(Section(label, getLabelStyle()));
}
refresh();
}
void DiscreteSlider::setSections(
const std::vector<TextWithEntities> &labels,
Text::MarkedContext context,
Fn<bool()> paused) {
Assert(!labels.empty());
context.repaint = [this] { update(); };
_sections.clear();
for (const auto &label : labels) {
_sections.push_back(Section(label, getLabelStyle(), context));
}
_paused = std::move(paused);
refresh();
}
void DiscreteSlider::refresh() {
stopAnimation();
if (_activeIndex >= _sections.size()) {
_activeIndex = 0;
}
if (_selected >= _sections.size()) {
_selected = 0;
}
resizeToWidth(width());
update();
}
DiscreteSlider::Range DiscreteSlider::getFinalActiveRange() const {
const auto raw = (_sections.empty() || _selected < 0)
? nullptr
: &_sections[_selected];
if (!raw) {
return { 0, 0 };
}
const auto width = _snapToLabel
? std::min(raw->width, raw->contentWidth)
: raw->width;
return { raw->left + ((raw->width - width) / 2), width };
}
DiscreteSlider::Range DiscreteSlider::getCurrentActiveRange() const {
const auto to = getFinalActiveRange();
return {
int(base::SafeRound(_a_left.value(to.left))),
int(base::SafeRound(_a_width.value(to.width))),
};
}
void DiscreteSlider::enumerateSections(Fn<bool(Section&)> callback) {
for (auto &section : _sections) {
if (!callback(section)) {
return;
}
}
}
void DiscreteSlider::enumerateSections(
Fn<bool(const Section&)> callback) const {
for (const auto &section : _sections) {
if (!callback(section)) {
return;
}
}
}
void DiscreteSlider::mousePressEvent(QMouseEvent *e) {
const auto index = getIndexFromPosition(e->pos());
if (_selectOnPress) {
setSelectedSection(index);
}
startRipple(index);
_pressed = index;
}
void DiscreteSlider::mouseMoveEvent(QMouseEvent *e) {
if (_pressed < 0) {
return;
}
if (_selectOnPress) {
setSelectedSection(getIndexFromPosition(e->pos()));
}
}
void DiscreteSlider::mouseReleaseEvent(QMouseEvent *e) {
const auto pressed = std::exchange(_pressed, -1);
if (pressed < 0) {
return;
}
const auto index = getIndexFromPosition(e->pos());
if (pressed < _sections.size()) {
if (_sections[pressed].ripple) {
_sections[pressed].ripple->lastStop();
}
}
if (_selectOnPress || index == pressed) {
setActiveSection(index);
}
}
void DiscreteSlider::setSelectedSection(int index) {
if (index >= int(_sections.size())) {
return;
}
if (_selected != index) {
const auto from = getFinalActiveRange();
_selected = index;
const auto to = getFinalActiveRange();
const auto duration = getAnimationDuration();
const auto updater = [this] { update(); };
_a_left.start(updater, from.left, to.left, duration);
_a_width.start(updater, from.width, to.width, duration);
_callbackAfterMs = crl::now() + duration;
}
}
int DiscreteSlider::getIndexFromPosition(QPoint pos) {
const auto count = _sections.size();
for (auto i = 0; i != count; ++i) {
if (_sections[i].left + _sections[i].width > pos.x()) {
return i;
}
}
return count - 1;
}
DiscreteSlider::Section::Section(
const QString &label,
const style::TextStyle &st)
: label(st, label)
, contentWidth(Section::label.maxWidth()) {
}
DiscreteSlider::Section::Section(
const TextWithEntities &label,
const style::TextStyle &st,
const Text::MarkedContext &context) {
this->label.setMarkedText(st, label, kMarkupTextOptions, context);
contentWidth = Section::label.maxWidth();
}
SettingsSlider::SettingsSlider(
QWidget *parent,
const style::SettingsSlider &st)
: DiscreteSlider(parent, st.barSnapToLabel)
, _st(st) {
if (_st.barRadius > 0) {
_bar.emplace(_st.barRadius, _st.barFg);
_barActive.emplace(_st.barRadius, _st.barFgActive);
}
setSelectOnPress(_st.ripple.showDuration == 0);
}
const style::SettingsSlider &SettingsSlider::st() const {
return _st;
}
int SettingsSlider::centerOfSection(int section) const {
const auto widths = countSectionsWidths(0);
auto result = 0;
if (section >= 0 && section < widths.size()) {
for (auto i = 0; i < section; i++) {
result += widths[i];
}
result += widths[section] / 2;
}
return result;
}
void SettingsSlider::fitWidthToSections() {
const auto widths = countSectionsWidths(0);
resizeToWidth(ranges::accumulate(widths, .0) + _st.padding * 2);
}
void SettingsSlider::setRippleTopRoundRadius(int radius) {
_rippleTopRoundRadius = radius;
}
const style::TextStyle &SettingsSlider::getLabelStyle() const {
return _st.labelStyle;
}
int SettingsSlider::getAnimationDuration() const {
return _st.duration;
}
void SettingsSlider::resizeSections(int newWidth) {
const auto count = getSectionsCount();
if (!count) {
return;
}
const auto sectionWidths = countSectionsWidths(newWidth);
auto skip = 0;
auto x = _st.padding * 1.;
auto sectionWidth = sectionWidths.begin();
enumerateSections([&](Section &section) {
Expects(sectionWidth != sectionWidths.end());
section.left = std::floor(x) + skip;
x += *sectionWidth;
section.width = qRound(x) - (section.left - skip);
skip += _st.barSkip;
++sectionWidth;
return true;
});
stopAnimation();
}
std::vector<float64> SettingsSlider::countSectionsWidths(int newWidth) const {
const auto count = getSectionsCount();
const auto sectionsWidth = newWidth
- 2 * _st.padding
- (count - 1) * _st.barSkip;
const auto sectionWidth = sectionsWidth / float64(count);
auto result = std::vector<float64>(count, sectionWidth);
auto labelsWidth = 0;
auto commonWidth = true;
enumerateSections([&](const Section &section) {
labelsWidth += section.contentWidth;
if (section.contentWidth >= sectionWidth) {
commonWidth = false;
}
return true;
});
// If labelsWidth > sectionsWidth we're screwed anyway.
if (_st.strictSkip || (!commonWidth && labelsWidth <= sectionsWidth)) {
auto padding = _st.strictSkip
? (_st.strictSkip / 2.)
: (sectionsWidth - labelsWidth) / (2. * count);
auto currentWidth = result.begin();
enumerateSections([&](const Section &section) {
Expects(currentWidth != result.end());
*currentWidth = padding + section.contentWidth + padding;
++currentWidth;
return true;
});
}
return result;
}
int SettingsSlider::resizeGetHeight(int newWidth) {
resizeSections(newWidth);
return _st.height;
}
void SettingsSlider::startRipple(int sectionIndex) {
if (!_st.ripple.showDuration) {
return;
}
auto index = 0;
enumerateSections([this, &index, sectionIndex](Section &section) {
if (index++ == sectionIndex) {
if (!section.ripple) {
auto mask = prepareRippleMask(sectionIndex, section);
section.ripple = std::make_unique<RippleAnimation>(
_st.ripple,
std::move(mask),
[this] { update(); });
}
const auto point = mapFromGlobal(QCursor::pos());
section.ripple->add(point - QPoint(section.left, 0));
return false;
}
return true;
});
}
QImage SettingsSlider::prepareRippleMask(
int sectionIndex,
const Section &section) {
const auto size = QSize(section.width, height() - _st.rippleBottomSkip);
if (!_rippleTopRoundRadius
|| (sectionIndex > 0 && sectionIndex + 1 < getSectionsCount())) {
return RippleAnimation::RectMask(size);
}
return RippleAnimation::MaskByDrawer(size, false, [&](QPainter &p) {
const auto plusRadius = _rippleTopRoundRadius + 1;
p.drawRoundedRect(
0,
0,
section.width,
height() + plusRadius,
_rippleTopRoundRadius,
_rippleTopRoundRadius);
if (sectionIndex > 0) {
p.fillRect(0, 0, plusRadius, plusRadius, p.brush());
}
if (sectionIndex + 1 < getSectionsCount()) {
p.fillRect(
section.width - plusRadius,
0,
plusRadius,
plusRadius, p.brush());
}
});
}
void SettingsSlider::paintEvent(QPaintEvent *e) {
auto p = QPainter(this);
const auto clip = e->rect();
const auto range = DiscreteSlider::getCurrentActiveRange();
const auto drawRect = [&](QRect rect, bool active = false) {
const auto &bar = active ? _barActive : _bar;
if (bar) {
bar->paint(p, rect);
} else {
p.fillRect(rect, active ? _st.barFgActive : _st.barFg);
}
};
enumerateSections([&](Section &section) {
const auto activeWidth = _st.barSnapToLabel
? section.contentWidth
: section.width;
const auto activeLeft = section.left
+ (section.width - activeWidth) / 2;
const auto divider = std::max(std::min(activeWidth, range.width), 1);
const auto active = 1.
- std::clamp(
std::abs(range.left - activeLeft) / float64(divider),
0.,
1.);
if (section.ripple) {
const auto color = anim::color(
_st.rippleBg,
_st.rippleBgActive,
active);
section.ripple->paint(p, section.left, 0, width(), &color);
if (section.ripple->empty()) {
section.ripple.reset();
}
}
if (!_st.barSnapToLabel) {
auto from = activeLeft;
auto tofill = activeWidth;
if (range.left > from) {
const auto fill = std::min(tofill, range.left - from);
drawRect(myrtlrect(from, _st.barTop, fill, _st.barStroke));
from += fill;
tofill -= fill;
}
if (range.left + activeWidth > from) {
const auto fill = std::min(
tofill,
range.left + activeWidth - from);
if (fill) {
drawRect(
myrtlrect(from, _st.barTop, fill, _st.barStroke),
true);
from += fill;
tofill -= fill;
}
}
if (tofill) {
drawRect(myrtlrect(from, _st.barTop, tofill, _st.barStroke));
}
}
const auto labelLeft = section.left
+ (section.width - section.contentWidth) / 2;
const auto rect = myrtlrect(
labelLeft,
_st.labelTop,
section.contentWidth,
_st.labelStyle.font->height);
if (rect.intersects(clip)) {
p.setPen(anim::pen(_st.labelFg, _st.labelFgActive, active));
section.label.draw(p, {
.position = QPoint(labelLeft, _st.labelTop),
.outerWidth = width(),
.availableWidth = section.label.maxWidth(),
.paused = paused(),
});
}
return true;
});
if (_st.barSnapToLabel) {
const auto add = _st.barStroke / 2;
const auto from = std::max(range.left - add, 0);
const auto till = std::min(range.left + range.width + add, width());
if (from < till) {
drawRect(
myrtlrect(from, _st.barTop, till - from, _st.barStroke),
true);
}
}
}
} // namespace Ui

View File

@@ -0,0 +1,172 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
#include "ui/rp_widget.h"
#include "ui/round_rect.h"
#include "ui/effects/animations.h"
#include "ui/text/text.h"
namespace style {
struct TextStyle;
struct SettingsSlider;
} // namespace style
namespace st {
extern const style::SettingsSlider &defaultSettingsSlider;
} // namespace st
namespace Ui {
class RippleAnimation;
class DiscreteSlider : public RpWidget {
public:
DiscreteSlider(QWidget *parent, bool snapToLabel);
~DiscreteSlider();
void addSection(const QString &label);
void addSection(
const TextWithEntities &label,
Text::MarkedContext context = {});
void setSections(const std::vector<QString> &labels);
void setSections(
const std::vector<TextWithEntities> &labels,
Text::MarkedContext context = {},
Fn<bool()> paused = nullptr);
int activeSection() const {
return _activeIndex;
}
void setActiveSection(int index);
void setActiveSectionFast(int index);
void finishAnimating();
void setAdditionalContentWidthToSection(int index, int width);
[[nodiscard]] rpl::producer<int> sectionActivated() const {
return _sectionActivated.events();
}
[[nodiscard]] int sectionsCount() const;
[[nodiscard]] int lookupSectionLeft(int index) const;
protected:
void timerEvent(QTimerEvent *e) override;
void mousePressEvent(QMouseEvent *e) override;
void mouseMoveEvent(QMouseEvent *e) override;
void mouseReleaseEvent(QMouseEvent *e) override;
int resizeGetHeight(int newWidth) override = 0;
struct Section {
Section(const QString &label, const style::TextStyle &st);
Section(
const TextWithEntities &label,
const style::TextStyle &st,
const Text::MarkedContext &context);
Text::String label;
std::unique_ptr<RippleAnimation> ripple;
int left = 0;
int width = 0;
int contentWidth = 0;
};
struct Range {
int left = 0;
int width = 0;
};
[[nodiscard]] Range getFinalActiveRange() const;
[[nodiscard]] Range getCurrentActiveRange() const;
[[nodiscard]] int getSectionsCount() const {
return _sections.size();
}
void enumerateSections(Fn<bool(Section&)> callback);
void enumerateSections(Fn<bool(const Section&)> callback) const;
virtual void startRipple(int sectionIndex) {
}
void stopAnimation() {
_a_left.stop();
_a_width.stop();
}
void refresh();
void setSelectOnPress(bool selectOnPress);
[[nodiscard]] std::vector<Section> &sectionsRef();
[[nodiscard]] bool paused() const;
private:
void activateCallback();
virtual const style::TextStyle &getLabelStyle() const = 0;
virtual int getAnimationDuration() const = 0;
int getIndexFromPosition(QPoint pos);
void setSelectedSection(int index);
std::vector<Section> _sections;
Fn<bool()> _paused;
int _activeIndex = 0;
bool _selectOnPress = true;
bool _snapToLabel = false;
rpl::event_stream<int> _sectionActivated;
int _pressed = -1;
int _selected = 0;
Ui::Animations::Simple _a_left;
Ui::Animations::Simple _a_width;
int _timerId = -1;
crl::time _callbackAfterMs = 0;
};
class SettingsSlider : public DiscreteSlider {
public:
SettingsSlider(
QWidget *parent,
const style::SettingsSlider &st = st::defaultSettingsSlider);
[[nodiscard]] const style::SettingsSlider &st() const;
[[nodiscard]] int centerOfSection(int section) const;
virtual void fitWidthToSections();
void setRippleTopRoundRadius(int radius);
protected:
void paintEvent(QPaintEvent *e) override;
int resizeGetHeight(int newWidth) override;
void startRipple(int sectionIndex) override;
std::vector<float64> countSectionsWidths(int newWidth) const;
private:
const style::TextStyle &getLabelStyle() const override;
int getAnimationDuration() const override;
QImage prepareRippleMask(int sectionIndex, const Section &section);
void resizeSections(int newWidth);
const style::SettingsSlider &_st;
std::optional<Ui::RoundRect> _bar;
std::optional<Ui::RoundRect> _barActive;
int _rippleTopRoundRadius = 0;
};
} // namespace Ui

View File

@@ -0,0 +1,237 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "ui/widgets/expandable_peer_list.h"
#include "data/data_peer.h"
#include "info/profile/info_profile_values.h"
#include "lang/lang_text_entity.h"
#include "ui/controls/userpic_button.h"
#include "ui/rect.h"
#include "ui/text/text_utilities.h"
#include "ui/vertical_list.h"
#include "ui/widgets/buttons.h"
#include "ui/widgets/participants_check_view.h"
#include "ui/wrap/slide_wrap.h"
#include "ui/wrap/vertical_layout.h"
#include "styles/style_boxes.h"
#include "styles/style_layers.h"
#include "styles/style_widgets.h"
namespace Ui {
namespace {
class Button final : public Ui::RippleButton {
public:
Button(not_null<QWidget*> parent, int count);
[[nodiscard]] not_null<Ui::AbstractCheckView*> checkView() const;
private:
void paintEvent(QPaintEvent *event) override;
QImage prepareRippleMask() const override;
QPoint prepareRippleStartPosition() const override;
std::unique_ptr<Ui::AbstractCheckView> _view;
};
Button::Button(not_null<QWidget*> parent, int count)
: Ui::RippleButton(parent, st::defaultRippleAnimation)
, _view(std::make_unique<Ui::ParticipantsCheckView>(
count,
st::slideWrapDuration,
false,
[=] { update(); })) {
}
not_null<Ui::AbstractCheckView*> Button::checkView() const {
return _view.get();
}
QImage Button::prepareRippleMask() const {
return _view->prepareRippleMask();
}
QPoint Button::prepareRippleStartPosition() const {
return mapFromGlobal(QCursor::pos());
}
void Button::paintEvent(QPaintEvent *event) {
auto p = QPainter(this);
Ui::RippleButton::paintRipple(p, QPoint());
_view->paint(p, 0, 0, width());
}
} // namespace
void AddExpandablePeerList(
not_null<Ui::Checkbox*> checkbox,
not_null<ExpandablePeerListController*> controller,
not_null<Ui::VerticalLayout*> inner) {
const auto &participants = controller->data.participants;
const auto hideRightButton = controller->data.hideRightButton;
const auto checkTopOnAllInner = controller->data.checkTopOnAllInner;
const auto isSingle = controller->data.skipSingle
? false
: (participants.size() == 1);
if (isSingle) {
const auto p = participants.front();
controller->collectRequests = [=] { return Participants{ p }; };
return;
}
const auto count = int(participants.size());
const auto button = !hideRightButton
? Ui::CreateChild<Button>(inner, count)
: nullptr;
if (button) {
button->resize(Ui::ParticipantsCheckView::ComputeSize(count));
}
const auto overlay = Ui::CreateChild<Ui::AbstractButton>(inner);
checkbox->geometryValue(
) | rpl::on_next([=](const QRect &rect) {
overlay->setGeometry(rect);
overlay->raise();
if (button) {
button->moveToRight(
st::moderateBoxExpandRight,
rect.top() + (rect.height() - button->height()) / 2,
inner->width());
button->raise();
}
}, overlay->lifetime());
controller->toggleRequestsFromInner.events(
) | rpl::on_next([=](bool toggled) {
checkbox->setChecked(toggled);
}, checkbox->lifetime());
if (button) {
button->setClickedCallback([=] {
button->checkView()->setChecked(
!button->checkView()->checked(),
anim::type::normal);
controller->toggleRequestsFromTop.fire_copy(
button->checkView()->checked());
});
}
overlay->setClickedCallback([=] {
checkbox->setChecked(!checkbox->checked());
controller->checkAllRequests.fire_copy(checkbox->checked());
});
{
const auto wrap = inner->add(
object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(
inner,
object_ptr<Ui::VerticalLayout>(inner)));
wrap->toggle(hideRightButton, anim::type::instant);
controller->toggleRequestsFromTop.events(
) | rpl::on_next([=](bool toggled) {
wrap->toggle(toggled, anim::type::normal);
}, wrap->lifetime());
const auto container = wrap->entity();
Ui::AddSkip(container);
auto &lifetime = wrap->lifetime();
const auto clicks = lifetime.make_state<rpl::event_stream<>>();
const auto checkboxes = ranges::views::all(
participants
) | ranges::views::transform([&](not_null<PeerData*> peer) {
const auto line = container->add(
object_ptr<Ui::AbstractButton>(container));
const auto &st = st::moderateBoxUserpic;
line->resize(line->width(), st.size.height());
using namespace Info::Profile;
auto name = controller->data.bold
? (NameValue(peer) | rpl::map(tr::bold) | rpl::type_erased)
: NameValue(peer) | rpl::map(tr::marked);
const auto userpic
= Ui::CreateChild<Ui::UserpicButton>(line, peer, st);
const auto checkbox = Ui::CreateChild<Ui::Checkbox>(
line,
controller->data.messagesCounts
? rpl::combine(
std::move(name),
rpl::duplicate(controller->data.messagesCounts)
) | rpl::map([=](const auto &richName, const auto &map) {
const auto it = map.find(peer->id);
return (it == map.end() || !it->second)
? richName
: TextWithEntities{
(u"(%1) "_q).arg(it->second)
}.append(richName);
})
: std::move(name) | rpl::type_erased,
st::defaultBoxCheckbox,
std::make_unique<Ui::CheckView>(
st::defaultCheck,
ranges::contains(controller->data.checked, peer->id)));
checkbox->setCheckAlignment(style::al_left);
rpl::combine(
line->widthValue(),
checkbox->widthValue()
) | rpl::on_next([=](int width, int) {
userpic->moveToLeft(
st::boxRowPadding.left()
+ checkbox->checkRect().width()
+ st::defaultBoxCheckbox.textPosition.x(),
0);
const auto skip = st::defaultBoxCheckbox.textPosition.x();
checkbox->resizeToWidth(width
- rect::right(userpic)
- skip
- st::boxRowPadding.right());
checkbox->moveToLeft(
rect::right(userpic) + skip,
((userpic->height() - checkbox->height()) / 2)
+ st::defaultBoxCheckbox.margin.top());
}, checkbox->lifetime());
userpic->setAttribute(Qt::WA_TransparentForMouseEvents);
checkbox->setAttribute(Qt::WA_TransparentForMouseEvents);
line->setClickedCallback([=] {
checkbox->setChecked(!checkbox->checked());
clicks->fire({});
});
return checkbox;
}) | ranges::to_vector;
clicks->events(
) | rpl::on_next([=] {
controller->toggleRequestsFromInner.fire_copy(
checkTopOnAllInner
? ranges::all_of(checkboxes, &Ui::Checkbox::checked)
: ranges::any_of(checkboxes, &Ui::Checkbox::checked));
}, container->lifetime());
controller->checkAllRequests.events(
) | rpl::on_next([=](bool checked) {
for (const auto &c : checkboxes) {
c->setChecked(checked);
}
}, container->lifetime());
controller->collectRequests = [=] {
auto result = Participants();
for (auto i = 0; i < checkboxes.size(); i++) {
if (checkboxes[i]->checked()) {
result.push_back(participants[i]);
}
}
return result;
};
}
}
} // namespace Ui

View File

@@ -0,0 +1,43 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
class PeerData;
using Participants = std::vector<not_null<PeerData*>>;
namespace Ui {
class Checkbox;
class VerticalLayout;
struct ExpandablePeerListController final {
struct Data final {
rpl::producer<base::flat_map<PeerId, int>> messagesCounts = nullptr;
Participants participants;
std::vector<PeerId> checked;
bool skipSingle = false;
bool hideRightButton = false;
bool checkTopOnAllInner = false;
bool bold = true;
};
ExpandablePeerListController(Data &&data) : data(std::move(data)) {
}
const Data data;
rpl::event_stream<bool> toggleRequestsFromTop;
rpl::event_stream<bool> toggleRequestsFromInner;
rpl::event_stream<bool> checkAllRequests;
Fn<Participants()> collectRequests;
};
void AddExpandablePeerList(
not_null<Ui::Checkbox*> checkbox,
not_null<ExpandablePeerListController*> controller,
not_null<Ui::VerticalLayout*> inner);
} // namespace Ui

View File

@@ -0,0 +1,459 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "ui/widgets/fields/special_fields.h"
#include "lang/lang_keys.h"
#include "countries/countries_instance.h" // Countries::ValidPhoneCode
#include "styles/style_widgets.h"
#include <QtCore/QRegularExpression>
namespace Ui {
namespace {
constexpr auto kMaxUsernameLength = 32;
// Rest of the phone number, without country code (seen 12 at least),
// need more for service numbers.
constexpr auto kMaxPhoneTailLength = 32;
// Max length of country phone code.
constexpr auto kMaxPhoneCodeLength = 4;
} // namespace
CountryCodeInput::CountryCodeInput(
QWidget *parent,
const style::InputField &st)
: MaskedInputField(parent, st) {
}
void CountryCodeInput::startErasing(QKeyEvent *e) {
setFocus();
keyPressEvent(e);
}
void CountryCodeInput::codeSelected(const QString &code) {
auto wasText = getLastText();
auto wasCursor = cursorPosition();
auto newText = '+' + code;
auto newCursor = int(newText.size());
setText(newText);
_nosignal = true;
correctValue(wasText, wasCursor, newText, newCursor);
_nosignal = false;
changed();
}
void CountryCodeInput::keyPressEvent(QKeyEvent *e) {
if (e->key() == Qt::Key_Space) {
_spacePressed.fire({});
} else {
MaskedInputField::keyPressEvent(e);
}
}
void CountryCodeInput::correctValue(
const QString &was,
int wasCursor,
QString &now,
int &nowCursor) {
QString newText, addToNumber;
int oldPos(nowCursor);
int newPos(-1);
int oldLen(now.length());
int start = 0;
int digits = 5;
newText.reserve(oldLen + 1);
if (oldLen && now[0] == '+') {
if (start == oldPos) {
newPos = newText.length();
}
++start;
}
newText += '+';
for (int i = start; i < oldLen; ++i) {
if (i == oldPos) {
newPos = newText.length();
}
auto ch = now[i];
if (ch.isDigit()) {
if (!digits || !--digits) {
addToNumber += ch;
} else {
newText += ch;
}
}
}
if (!addToNumber.isEmpty()) {
auto validCode = Countries::Instance().validPhoneCode(newText.mid(1));
addToNumber = newText.mid(1 + validCode.length()) + addToNumber;
newText = '+' + validCode;
}
setCorrectedText(now, nowCursor, newText, newPos);
if (!_nosignal && was != newText) {
_codeChanged.fire(newText.mid(1));
}
if (!addToNumber.isEmpty()) {
_addedToNumber.fire_copy(addToNumber);
}
}
PhonePartInput::PhonePartInput(
QWidget *parent,
const style::InputField &st,
PhonePartInput::GroupsCallback groupsCallback)
: MaskedInputField(parent, st/*, tr::lng_phone_ph(tr::now)*/)
, _groupsCallback(std::move(groupsCallback)) {
}
void PhonePartInput::paintAdditionalPlaceholder(QPainter &p) {
if (!_pattern.isEmpty()) {
auto t = getDisplayedText();
auto ph = _additionalPlaceholder.mid(t.size());
if (!ph.isEmpty()) {
p.setClipRect(rect());
auto phRect = placeholderRect();
int tw = phFont()->width(t);
if (tw < phRect.width()) {
phRect.setLeft(phRect.left() + tw);
placeholderAdditionalPrepare(p);
p.drawText(phRect, ph, style::al_topleft);
}
}
}
}
void PhonePartInput::keyPressEvent(QKeyEvent *e) {
if (e->key() == Qt::Key_Backspace && cursorPosition() == 0) {
_frontBackspaceEvent.fire_copy(e);
} else {
MaskedInputField::keyPressEvent(e);
}
}
void PhonePartInput::correctValue(
const QString &was,
int wasCursor,
QString &now,
int &nowCursor) {
if (!now.isEmpty() && (_lastDigits != now)) {
_lastDigits = now;
_lastDigits.replace(TextUtilities::RegExpDigitsExclude(), QString());
updatePattern(_groupsCallback(_code + _lastDigits));
}
QString newText;
int oldPos(nowCursor), newPos(-1), oldLen(now.length()), digitCount = 0;
for (int i = 0; i < oldLen; ++i) {
if (now[i].isDigit()) {
++digitCount;
}
}
if (digitCount > kMaxPhoneTailLength) {
digitCount = kMaxPhoneTailLength;
}
bool inPart = !_pattern.isEmpty();
int curPart = -1, leftInPart = 0;
newText.reserve(oldLen);
for (int i = 0; i < oldLen; ++i) {
if (i == oldPos && newPos < 0) {
newPos = newText.length();
}
auto ch = now[i];
if (ch.isDigit()) {
if (!digitCount--) {
break;
}
if (inPart) {
if (leftInPart) {
--leftInPart;
} else {
++curPart;
inPart = curPart < _pattern.size();
// Don't add an extra space to the end.
if (inPart) {
newText += ' ';
}
leftInPart = inPart ? (_pattern.at(curPart) - 1) : 0;
++oldPos;
}
}
newText += ch;
} else if (ch == ' ' || ch == '-' || ch == '(' || ch == ')') {
if (inPart) {
if (leftInPart) {
} else {
newText += ch;
++curPart;
inPart = curPart < _pattern.size();
leftInPart = inPart ? _pattern.at(curPart) : 0;
}
} else {
newText += ch;
}
}
}
auto newlen = newText.size();
while (newlen > 0 && newText.at(newlen - 1).isSpace()) {
--newlen;
}
if (newlen < newText.size()) {
newText = newText.mid(0, newlen);
}
setCorrectedText(now, nowCursor, newText, newPos);
}
void PhonePartInput::addedToNumber(const QString &added) {
setFocus();
auto wasText = getLastText();
auto wasCursor = cursorPosition();
auto newText = added + wasText;
auto newCursor = int(newText.size());
setText(newText);
setCursorPosition(added.length());
correctValue(wasText, wasCursor, newText, newCursor);
startPlaceholderAnimation();
}
void PhonePartInput::chooseCode(const QString &code) {
_code = code;
updatePattern(_groupsCallback(_code));
auto wasText = getLastText();
auto wasCursor = cursorPosition();
auto newText = getLastText();
auto newCursor = int(newText.size());
correctValue(wasText, wasCursor, newText, newCursor);
startPlaceholderAnimation();
update();
}
void PhonePartInput::updatePattern(QVector<int> &&pattern) {
if (_pattern == pattern) {
return;
}
_pattern = std::move(pattern);
if (!_pattern.isEmpty() && _pattern.at(0) == _code.size()) {
_pattern.pop_front();
} else {
_pattern.clear();
}
_additionalPlaceholder = QString();
if (!_pattern.isEmpty()) {
_additionalPlaceholder.reserve(20);
for (const auto &part : _pattern) {
_additionalPlaceholder.append(' ');
_additionalPlaceholder.append(QString(part, QChar(0x2212)));
}
}
setPlaceholderHidden(!_additionalPlaceholder.isEmpty());
}
UsernameInput::UsernameInput(
QWidget *parent,
const style::InputField &st,
rpl::producer<QString> placeholder,
const QString &val,
const QString &linkPlaceholder)
: MaskedInputField(parent, st, std::move(placeholder), val) {
setLinkPlaceholder(linkPlaceholder);
}
void UsernameInput::setLinkPlaceholder(const QString &placeholder) {
_linkPlaceholder = placeholder;
if (!_linkPlaceholder.isEmpty()) {
setTextMargins(style::margins(_st.textMargins.left() + _st.style.font->width(_linkPlaceholder), _st.textMargins.top(), _st.textMargins.right(), _st.textMargins.bottom()));
setPlaceholderHidden(true);
}
}
void UsernameInput::paintAdditionalPlaceholder(QPainter &p) {
if (!_linkPlaceholder.isEmpty()) {
p.setFont(_st.style.font);
p.setPen(_st.placeholderFg);
p.drawText(QRect(_st.textMargins.left(), _st.textMargins.top(), width(), height() - _st.textMargins.top() - _st.textMargins.bottom()), _linkPlaceholder, style::al_topleft);
}
}
void UsernameInput::correctValue(
const QString &was,
int wasCursor,
QString &now,
int &nowCursor) {
auto newPos = nowCursor;
auto from = 0, len = int(now.size());
for (; from < len; ++from) {
if (!now.at(from).isSpace()) {
break;
}
if (newPos > 0) --newPos;
}
len -= from;
if (len > kMaxUsernameLength) {
len = kMaxUsernameLength + (now.at(from) == '@' ? 1 : 0);
}
for (int32 to = from + len; to > from;) {
--to;
if (!now.at(to).isSpace()) {
break;
}
--len;
}
setCorrectedText(now, nowCursor, now.mid(from, len), newPos);
}
PhoneInput::PhoneInput(
QWidget *parent,
const style::InputField &st,
rpl::producer<QString> placeholder,
const QString &defaultValue,
QString value,
PhoneInput::GroupsCallback groupsCallback)
: MaskedInputField(parent, st, std::move(placeholder), value)
, _defaultValue(defaultValue)
, _groupsCallback(std::move(groupsCallback)) {
if (value.isEmpty()) {
clearText();
} else {
auto pos = int(value.size());
correctValue(QString(), 0, value, pos);
}
}
void PhoneInput::focusInEvent(QFocusEvent *e) {
MaskedInputField::focusInEvent(e);
setSelection(cursorPosition(), cursorPosition());
}
void PhoneInput::clearText() {
auto value = _defaultValue;
setText(value);
auto pos = int(value.size());
correctValue(QString(), 0, value, pos);
}
void PhoneInput::paintAdditionalPlaceholder(QPainter &p) {
if (!_pattern.isEmpty()) {
auto t = getDisplayedText();
auto ph = _additionalPlaceholder.mid(t.size());
if (!ph.isEmpty()) {
p.setClipRect(rect());
auto phRect = placeholderRect();
int tw = phFont()->width(t);
if (tw < phRect.width()) {
phRect.setLeft(phRect.left() + tw);
placeholderAdditionalPrepare(p);
p.drawText(phRect, ph, style::al_topleft);
}
}
}
}
void PhoneInput::correctValue(
const QString &was,
int wasCursor,
QString &now,
int &nowCursor) {
auto digits = now;
digits.replace(TextUtilities::RegExpDigitsExclude(), QString());
_pattern = _groupsCallback(digits);
QString newPlaceholder;
if (_pattern.isEmpty()) {
newPlaceholder = QString();
} else if (_pattern.size() == 1 && _pattern.at(0) == digits.size()) {
newPlaceholder = QString(_pattern.at(0) + 2, ' ') + tr::lng_contact_phone(tr::now);
} else {
newPlaceholder.reserve(20);
for (int i = 0, l = _pattern.size(); i < l; ++i) {
if (i) {
newPlaceholder.append(' ');
} else {
newPlaceholder.append('+');
}
newPlaceholder.append(i ? QString(_pattern.at(i), QChar(0x2212)) : digits.mid(0, _pattern.at(i)));
}
}
if (_additionalPlaceholder != newPlaceholder) {
_additionalPlaceholder = newPlaceholder;
setPlaceholderHidden(!_additionalPlaceholder.isEmpty());
update();
}
QString newText;
int oldPos(nowCursor), newPos(-1), oldLen(now.length()), digitCount = qMin(digits.size(), kMaxPhoneCodeLength + kMaxPhoneTailLength);
bool inPart = !_pattern.isEmpty(), plusFound = false;
int curPart = 0, leftInPart = inPart ? _pattern.at(curPart) : 0;
newText.reserve(oldLen + 1);
newText.append('+');
for (int i = 0; i < oldLen; ++i) {
if (i == oldPos && newPos < 0) {
newPos = newText.length();
}
QChar ch(now[i]);
if (ch.isDigit()) {
if (!digitCount--) {
break;
}
if (inPart) {
if (leftInPart) {
--leftInPart;
} else {
++curPart;
inPart = curPart < _pattern.size();
// Don't add an extra space to the end.
if (inPart) {
newText += ' ';
}
leftInPart = inPart ? (_pattern.at(curPart) - 1) : 0;
++oldPos;
}
}
newText += ch;
} else if (ch == ' ' || ch == '-' || ch == '(' || ch == ')') {
if (inPart) {
if (leftInPart) {
} else {
newText += ch;
++curPart;
inPart = curPart < _pattern.size();
leftInPart = inPart ? _pattern.at(curPart) : 0;
}
} else {
newText += ch;
}
} else if (ch == '+') {
plusFound = true;
}
}
if (!plusFound && newText == u"+"_q) {
newText = QString();
newPos = 0;
}
int32 newlen = newText.size();
while (newlen > 0 && newText.at(newlen - 1).isSpace()) {
--newlen;
}
if (newlen < newText.size()) {
newText = newText.mid(0, newlen);
}
setCorrectedText(now, nowCursor, newText, newPos);
}
} // namespace Ui

View File

@@ -0,0 +1,144 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
#include "ui/widgets/fields/masked_input_field.h"
namespace Ui {
class CountryCodeInput : public MaskedInputField {
public:
CountryCodeInput(QWidget *parent, const style::InputField &st);
void startErasing(QKeyEvent *e);
[[nodiscard]] rpl::producer<QString> addedToNumber() const {
return _addedToNumber.events();
}
[[nodiscard]] rpl::producer<QString> codeChanged() const {
return _codeChanged.events();
}
[[nodiscard]] rpl::producer<> spacePressed() const {
return _spacePressed.events();
}
void codeSelected(const QString &code);
protected:
void keyPressEvent(QKeyEvent *e) override;
void correctValue(
const QString &was,
int wasCursor,
QString &now,
int &nowCursor) override;
private:
bool _nosignal = false;
rpl::event_stream<QString> _addedToNumber;
rpl::event_stream<QString> _codeChanged;
rpl::event_stream<> _spacePressed;
};
class PhonePartInput : public MaskedInputField {
public:
using GroupsCallback = Fn<QVector<int>(const QString &)>;
PhonePartInput(
QWidget *parent,
const style::InputField &st,
GroupsCallback groupsCallback);
[[nodiscard]] auto frontBackspaceEvent() const
-> rpl::producer<not_null<QKeyEvent*>> {
return _frontBackspaceEvent.events();
}
void addedToNumber(const QString &added);
void chooseCode(const QString &code);
protected:
void keyPressEvent(QKeyEvent *e) override;
void correctValue(
const QString &was,
int wasCursor,
QString &now,
int &nowCursor) override;
void paintAdditionalPlaceholder(QPainter &p) override;
private:
void updatePattern(QVector<int> &&pattern);
QString _code;
QString _lastDigits;
QVector<int> _pattern;
QString _additionalPlaceholder;
rpl::event_stream<not_null<QKeyEvent*>> _frontBackspaceEvent;
GroupsCallback _groupsCallback;
};
class UsernameInput : public MaskedInputField {
public:
UsernameInput(
QWidget *parent,
const style::InputField &st,
rpl::producer<QString> placeholder,
const QString &val,
const QString &linkPlaceholder);
void setLinkPlaceholder(const QString &placeholder);
protected:
void correctValue(
const QString &was,
int wasCursor,
QString &now,
int &nowCursor) override;
void paintAdditionalPlaceholder(QPainter &p) override;
private:
QString _linkPlaceholder;
};
class PhoneInput : public MaskedInputField {
public:
using GroupsCallback = Fn<QVector<int>(const QString &)>;
PhoneInput(
QWidget *parent,
const style::InputField &st,
rpl::producer<QString> placeholder,
const QString &defaultValue,
QString value,
GroupsCallback groupsCallback);
void clearText();
protected:
void focusInEvent(QFocusEvent *e) override;
void correctValue(
const QString &was,
int wasCursor,
QString &now,
int &nowCursor) override;
void paintAdditionalPlaceholder(QPainter &p) override;
private:
QString _defaultValue;
QVector<int> _pattern;
QString _additionalPlaceholder;
GroupsCallback _groupsCallback;
};
} // namespace Ui

View File

@@ -0,0 +1,62 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "ui/widgets/fields/time_part_input_with_placeholder.h"
#include "lang/lang_numbers_animation.h"
namespace Ui {
void TimePartWithPlaceholder::setPhrase(
const tr::phrase<lngtag_count> &phrase) {
_phrase = phrase;
}
void TimePartWithPlaceholder::paintAdditionalPlaceholder(QPainter &p) {
maybeUpdatePlaceholder();
p.setClipRect(rect());
const auto phRect = placeholderRect();
if (_lastPlaceholder.width < phRect.width()) {
placeholderAdditionalPrepare(p);
p.drawText(
phRect.translated(-_lastPlaceholder.leftOffset, 0),
_lastPlaceholder.text,
style::al_left);
}
}
void TimePartWithPlaceholder::maybeUpdatePlaceholder() {
const auto displayedText = getDisplayedText();
if (displayedText == _lastPlaceholder.displayedText) {
return;
}
const auto count = displayedText.toUInt();
const auto textWithOffset = _phrase(
tr::now,
lt_count,
count,
Ui::StringWithNumbers::FromString);
_lastPlaceholder = {
.width = phFont()->width(textWithOffset.text),
.text = textWithOffset.text,
.leftOffset = phFont()->width(
textWithOffset.text.mid(0, textWithOffset.offset)),
.displayedText = displayedText,
};
if (displayedText.size() > 1 && displayedText.startsWith(_zero)) {
_lastPlaceholder.text.insert(textWithOffset.offset, _zero);
}
const auto leftMargins = (width() - _lastPlaceholder.width) / 2
+ _lastPlaceholder.leftOffset;
setTextMargins({ leftMargins, 0, 0, 0 });
}
} // namespace Ui

View File

@@ -0,0 +1,40 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
#include "ui/widgets/fields/time_part_input.h"
#include "lang_auto.h"
namespace Ui {
class TimePartWithPlaceholder final : public TimePart {
public:
using Ui::TimePart::TimePart;
void setPhrase(const tr::phrase<lngtag_count> &phrase);
protected:
void paintAdditionalPlaceholder(QPainter &p) override;
private:
void maybeUpdatePlaceholder();
const QChar _zero = QChar('0');
tr::phrase<lngtag_count> _phrase;
struct {
int width = 0;
QString text;
int leftOffset = 0;
QString displayedText;
} _lastPlaceholder;
};
} // namespace Ui

View File

@@ -0,0 +1,94 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "ui/widgets/gradient_round_button.h"
#include "ui/image/image_prepare.h"
#include "styles/style_boxes.h"
namespace Ui {
GradientButton::GradientButton(QWidget *widget, QGradientStops stops)
: RippleButton(widget, st::defaultRippleAnimation)
, _stops(std::move(stops)) {
}
void GradientButton::paintEvent(QPaintEvent *e) {
QPainter p(this);
validateBg();
p.drawImage(0, 0, _bg);
paintGlare(p);
const auto ripple = QColor(0, 0, 0, 36);
paintRipple(p, 0, 0, &ripple);
}
void GradientButton::paintGlare(QPainter &p) {
if (!_glare.glare.birthTime) {
return;
}
const auto progress = _glare.progress(crl::now());
const auto x = (-_glare.width) + (width() + _glare.width * 2) * progress;
const auto h = height();
const auto edgeWidth = _glare.width + st::roundRadiusLarge;
if (x > edgeWidth && x < (width() - edgeWidth)) {
p.drawTiledPixmap(x, 0, _glare.width, h, _glare.pixmap, 0, 0);
} else {
auto frame = QImage(
QSize(_glare.width, h) * style::DevicePixelRatio(),
QImage::Format_ARGB32_Premultiplied);
frame.setDevicePixelRatio(style::DevicePixelRatio());
frame.fill(Qt::transparent);
{
auto q = QPainter(&frame);
q.drawTiledPixmap(0, 0, _glare.width, h, _glare.pixmap, 0, 0);
q.setCompositionMode(QPainter::CompositionMode_DestinationIn);
q.drawImage(-x, 0, _bg, 0, 0);
}
p.drawImage(x, 0, frame);
}
}
void GradientButton::validateBg() {
const auto factor = devicePixelRatio();
if (!_bg.isNull()
&& (_bg.devicePixelRatio() == factor)
&& (_bg.size() == size() * factor)) {
return;
}
_bg = QImage(size() * factor, QImage::Format_ARGB32_Premultiplied);
_bg.setDevicePixelRatio(factor);
auto p = QPainter(&_bg);
auto gradient = QLinearGradient(QPointF(0, 0), QPointF(width(), 0));
gradient.setStops(_stops);
p.fillRect(rect(), gradient);
p.end();
_bg = Images::Round(std::move(_bg), ImageRoundRadius::Large);
}
void GradientButton::setGlarePaused(bool paused) {
_glare.paused = paused;
}
void GradientButton::validateGlare() {
_glare.validate(
st::premiumButtonFg->c,
[=] { update(); },
st::gradientButtonGlareTimeout,
st::gradientButtonGlareDuration);
}
void GradientButton::startGlareAnimation() {
validateGlare();
}
} // namespace Ui

View File

@@ -0,0 +1,35 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
#include "ui/widgets/buttons.h"
#include "ui/effects/glare.h"
namespace Ui {
class GradientButton final : public Ui::RippleButton {
public:
GradientButton(QWidget *widget, QGradientStops stops);
void startGlareAnimation();
void setGlarePaused(bool paused);
private:
void paintEvent(QPaintEvent *e);
void paintGlare(QPainter &p);
void validateBg();
void validateGlare();
QGradientStops _stops;
QImage _bg;
GlareEffect _glare;
};
} // namespace Ui

View File

@@ -0,0 +1,56 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "ui/widgets/horizontal_fit_container.h"
#include "ui/widgets/buttons.h"
namespace Ui {
HorizontalFitContainer::HorizontalFitContainer(
not_null<RpWidget*> parent,
int spacing)
: RpWidget(parent)
, _spacing(spacing) {
sizeValue() | rpl::on_next([=](QSize size) {
updateLayout(size);
}, lifetime());
}
int HorizontalFitContainer::add(not_null<AbstractButton*> button) {
const auto id = _nextId++;
_buttons.emplace(id, base::unique_qptr<AbstractButton>{ button.get() });
button->setParent(this);
button->show();
updateLayout(size());
return id;
}
void HorizontalFitContainer::remove(int id) {
if (const auto it = _buttons.find(id); it != _buttons.end()) {
_buttons.erase(it);
updateLayout(size());
}
}
void HorizontalFitContainer::updateLayout(QSize size) {
_layoutLifetime.destroy();
if (_buttons.empty()) {
return;
}
const auto count = int(_buttons.size());
const auto totalSpacing = _spacing * (count - 1);
const auto buttonWidth = (size.width() - totalSpacing) / count;
const auto h = size.height();
auto x = 0;
for (const auto &[id, button] : _buttons) {
button->setGeometry(x, 0, buttonWidth, h);
x += buttonWidth + _spacing;
}
}
} // namespace Ui

View File

@@ -0,0 +1,36 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
#include "ui/rp_widget.h"
#include "base/unique_qptr.h"
namespace Ui {
class AbstractButton;
class HorizontalFitContainer final : public RpWidget {
public:
HorizontalFitContainer(
not_null<RpWidget*> parent,
int spacing);
int add(not_null<AbstractButton*> button);
void remove(int id);
private:
void updateLayout(QSize size);
const int _spacing;
std::map<int, base::unique_qptr<AbstractButton>> _buttons;
int _nextId = 0;
rpl::lifetime _layoutLifetime;
};
} // namespace Ui

View File

@@ -0,0 +1,48 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "ui/widgets/level_meter.h"
#include "ui/painter.h"
#include "styles/style_widgets.h"
namespace Ui {
LevelMeter::LevelMeter(QWidget *parent, const style::LevelMeter &st)
: RpWidget(parent)
, _st(st) {
}
void LevelMeter::setValue(float value) {
_value = value;
repaint();
}
void LevelMeter::paintEvent(QPaintEvent* event) {
auto p = QPainter(this);
auto hq = PainterHighQualityEnabler(p);
p.setPen(Qt::NoPen);
const auto activeFg = _st.activeFg;
const auto inactiveFg = _st.inactiveFg;
const auto radius = _st.lineWidth / 2;
const auto rect = QRect(0, 0, _st.lineWidth, height());
p.setBrush(activeFg);
for (auto i = 0; i < _st.lineCount; ++i) {
const auto valueAtLine = (float64)(i + 1) / _st.lineCount;
if (valueAtLine > _value) {
p.setBrush(inactiveFg);
}
p.drawRoundedRect(
rect.translated((_st.lineWidth + _st.lineSpacing) * i, 0),
radius,
radius);
}
}
} // namespace Ui

View File

@@ -0,0 +1,33 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
#include "ui/rp_widget.h"
namespace style {
struct LevelMeter;
} // namespace style
namespace Ui {
class LevelMeter : public RpWidget {
public:
LevelMeter(QWidget *parent, const style::LevelMeter &st);
void setValue(float value);
protected:
void paintEvent(QPaintEvent *e) override;
private:
const style::LevelMeter &_st;
float _value = 0.0f;
};
} // namespace Ui

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,75 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
#include "base/object_ptr.h"
#include "ui/rp_widget.h"
namespace style {
struct MultiSelect;
} // namespace style
namespace Ui {
class ScrollArea;
class MultiSelect : public RpWidget {
public:
MultiSelect(
QWidget *parent,
const style::MultiSelect &st,
rpl::producer<QString> placeholder = nullptr,
const QString &query = QString());
[[nodiscard]] QString getQuery() const;
void setQuery(const QString &query);
void setInnerFocus();
void clearQuery();
void setQueryChangedCallback(Fn<void(const QString &query)> callback);
void setSubmittedCallback(Fn<void(Qt::KeyboardModifiers)> callback);
void setCancelledCallback(Fn<void()> callback);
void setResizedCallback(Fn<void()> callback);
enum class AddItemWay {
Default,
SkipAnimation,
};
using PaintRoundImage = Fn<void(Painter &p, int x, int y, int outerWidth, int size)>;
void addItem(uint64 itemId, const QString &text, style::color color, PaintRoundImage paintRoundImage, AddItemWay way = AddItemWay::Default);
void addItemInBunch(uint64 itemId, const QString &text, style::color color, PaintRoundImage paintRoundImage);
void finishItemsBunch();
void setItemText(uint64 itemId, const QString &text);
void setItemRemovedCallback(Fn<void(uint64 itemId)> callback);
void removeItem(uint64 itemId);
int getItemsCount() const;
QVector<uint64> getItems() const;
bool hasItem(uint64 itemId) const;
protected:
int resizeGetHeight(int newWidth) override;
bool eventFilter(QObject *o, QEvent *e) override;
private:
void scrollTo(int activeTop, int activeBottom);
const style::MultiSelect &_st;
object_ptr<Ui::ScrollArea> _scroll;
class Inner;
QPointer<Inner> _inner;
Fn<void()> _resizedCallback;
Fn<void(const QString &query)> _queryChangedCallback;
};
} // namespace Ui

View File

@@ -0,0 +1,95 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "ui/widgets/participants_check_view.h"
#include "ui/effects/ripple_animation.h"
#include "ui/effects/toggle_arrow.h"
#include "ui/painter.h"
#include "ui/rect.h"
#include "styles/style_boxes.h"
namespace Ui {
ParticipantsCheckView::ParticipantsCheckView(
int count,
int duration,
bool checked,
Fn<void()> updateCallback)
: Ui::AbstractCheckView(duration, checked, std::move(updateCallback))
, _text(QString::number(std::abs(count)))
, _count(count) {
}
QSize ParticipantsCheckView::ComputeSize(int count) {
return QSize(
st::moderateBoxExpandHeight
+ st::moderateBoxExpand.width()
+ st::moderateBoxExpandInnerSkip * 4
+ st::moderateBoxExpandFont->width(
QString::number(std::abs(count)))
+ st::moderateBoxExpandToggleSize,
st::moderateBoxExpandHeight);
}
QSize ParticipantsCheckView::getSize() const {
return ComputeSize(_count);
}
void ParticipantsCheckView::paint(
QPainter &p,
int left,
int top,
int outerWidth) {
auto hq = PainterHighQualityEnabler(p);
const auto size = getSize();
const auto radius = size.height() / 2;
p.setPen(Qt::NoPen);
st::moderateBoxExpand.paint(
p,
radius,
left + (size.height() - st::moderateBoxExpand.height()) / 2,
top + size.width());
const auto innerSkip = st::moderateBoxExpandInnerSkip;
p.setBrush(Qt::NoBrush);
p.setPen(st::boxTextFg);
p.setFont(st::moderateBoxExpandFont);
p.drawText(
QRect(
left + innerSkip + radius + st::moderateBoxExpand.width(),
top,
size.width(),
size.height()),
_text,
style::al_left);
const auto path = Ui::ToggleUpDownArrowPath(
left + size.width() - st::moderateBoxExpandToggleSize - radius,
top + size.height() / 2,
st::moderateBoxExpandToggleSize,
st::moderateBoxExpandToggleFourStrokes,
Ui::AbstractCheckView::currentAnimationValue());
p.fillPath(path, st::boxTextFg);
}
QImage ParticipantsCheckView::prepareRippleMask() const {
const auto size = getSize();
return Ui::RippleAnimation::RoundRectMask(size, size.height() / 2);
}
bool ParticipantsCheckView::checkRippleStartPosition(QPoint position) const {
return Rect(getSize()).contains(position);
}
void ParticipantsCheckView::checkedChangedHook(anim::type animated) {
}
ParticipantsCheckView::~ParticipantsCheckView() = default;
} // namespace Ui

View File

@@ -0,0 +1,39 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
#include "ui/widgets/checkbox.h"
namespace Ui {
class ParticipantsCheckView : public Ui::AbstractCheckView {
public:
ParticipantsCheckView(
int count,
int duration,
bool checked,
Fn<void()> updateCallback);
[[nodiscard]] static QSize ComputeSize(int count);
QSize getSize() const override;
void paint(QPainter &p, int left, int top, int outerWidth) override;
QImage prepareRippleMask() const override;
bool checkRippleStartPosition(QPoint position) const override;
~ParticipantsCheckView();
private:
const QString _text;
const int _count;
void checkedChangedHook(anim::type animated) override;
};
} // namespace Ui

View File

@@ -0,0 +1,72 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "ui/widgets/peer_bubble.h"
#include "data/data_peer.h"
#include "info/profile/info_profile_values.h"
#include "ui/controls/userpic_button.h"
#include "ui/painter.h"
#include "ui/rect.h"
#include "ui/widgets/labels.h"
#include "styles/style_boxes.h"
#include "styles/style_channel_earn.h"
#include "styles/style_chat.h"
#include "styles/style_layers.h"
namespace Ui {
object_ptr<Ui::RpWidget> CreatePeerBubble(
not_null<Ui::RpWidget*> parent,
not_null<PeerData*> peer) {
auto owned = object_ptr<Ui::RpWidget>(parent);
const auto peerBubble = owned.data();
peerBubble->setAttribute(Qt::WA_TransparentForMouseEvents);
const auto left = Ui::CreateChild<Ui::UserpicButton>(
peerBubble,
peer,
st::uploadUserpicButton);
const auto right = Ui::CreateChild<Ui::FlatLabel>(
peerBubble,
Info::Profile::NameValue(peer),
st::channelEarnSemiboldLabel);
const auto padding = st::chatGiveawayPeerPadding
+ QMargins(st::chatGiveawayPeerPadding.left(), 0, 0, 0);
rpl::combine(
left->sizeValue(),
right->sizeValue()
) | rpl::on_next([=](
const QSize &leftSize,
const QSize &rightSize) {
peerBubble->setNaturalWidth(
leftSize.width() + rightSize.width() + rect::m::sum::h(padding));
peerBubble->resize(peerBubble->naturalWidth(), leftSize.height());
left->moveToLeft(0, 0);
right->moveToRight(padding.right() + st::lineWidth, padding.top());
const auto maxRightSize = parent->width()
- rect::m::sum::h(st::boxRowPadding)
- rect::m::sum::h(padding)
- leftSize.width();
if ((rightSize.width() > maxRightSize) && (maxRightSize > 0)) {
right->resizeToWidth(maxRightSize);
}
}, peerBubble->lifetime());
peerBubble->paintRequest(
) | rpl::on_next([=] {
auto p = QPainter(peerBubble);
auto hq = PainterHighQualityEnabler(p);
p.setPen(Qt::NoPen);
p.setBrush(st::windowBgOver);
const auto rect = peerBubble->rect();
const auto radius = rect.height() / 2;
p.drawRoundedRect(rect, radius, radius);
}, peerBubble->lifetime());
return owned;
}
} // namespace Ui

View File

@@ -0,0 +1,26 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
template <typename Object>
class object_ptr;
class PeerData;
namespace Ui {
class RpWidget;
class VerticalLayout;
} // namespace Ui
namespace Ui {
[[nodiscard]] object_ptr<Ui::RpWidget> CreatePeerBubble(
not_null<Ui::RpWidget*> parent,
not_null<PeerData*> peer);
} // namespace Ui

View File

@@ -0,0 +1,160 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "ui/widgets/sent_code_field.h"
#include "lang/lang_keys.h"
#include <QRegularExpression>
namespace Ui {
SentCodeField::SentCodeField(
QWidget *parent,
const style::InputField &st,
rpl::producer<QString> placeholder,
const QString &val)
: Ui::InputField(parent, st, std::move(placeholder), val) {
changes() | rpl::on_next([=] { fix(); }, lifetime());
}
void SentCodeField::setAutoSubmit(int length, Fn<void()> submitCallback) {
_autoSubmitLength = length;
_submitCallback = std::move(submitCallback);
}
void SentCodeField::setChangedCallback(Fn<void()> changedCallback) {
_changedCallback = std::move(changedCallback);
}
QString SentCodeField::getDigitsOnly() const {
return QString(
getLastText()
).remove(TextUtilities::RegExpDigitsExclude());
}
void SentCodeField::fix() {
if (_fixing) return;
_fixing = true;
auto newText = QString();
const auto now = getLastText();
auto oldPos = textCursor().position();
auto newPos = -1;
auto oldLen = now.size();
auto digitCount = 0;
for (const auto &ch : now) {
if (ch.isDigit()) {
++digitCount;
}
}
if (_autoSubmitLength > 0 && digitCount > _autoSubmitLength) {
digitCount = _autoSubmitLength;
}
const auto strict = (_autoSubmitLength > 0)
&& (digitCount == _autoSubmitLength);
newText.reserve(oldLen);
int i = 0;
for (const auto &ch : now) {
if (i++ == oldPos) {
newPos = newText.length();
}
if (ch.isDigit()) {
if (!digitCount--) {
break;
}
newText += ch;
if (strict && !digitCount) {
break;
}
} else if (ch == '-') {
newText += ch;
}
}
if (newPos < 0) {
newPos = newText.length();
}
if (newText != now) {
setText(newText);
setCursorPosition(newPos);
}
_fixing = false;
if (_changedCallback) {
_changedCallback();
}
if (strict && _submitCallback) {
_submitCallback();
}
}
SentCodeCall::SentCodeCall(
FnMut<void()> callCallback,
Fn<void()> updateCallback)
: _call(std::move(callCallback))
, _update(std::move(updateCallback)) {
_timer.setCallback([=] {
if (_status.state == State::Waiting) {
if (--_status.timeout <= 0) {
_status.state = State::Calling;
_timer.cancel();
if (_call) {
_call();
}
}
}
if (_update) {
_update();
}
});
}
void SentCodeCall::setStatus(const Status &status) {
_status = status;
if (_status.state == State::Waiting) {
_timer.callEach(1000);
}
}
QString SentCodeCall::getText() const {
switch (_status.state) {
case State::Waiting: {
if (_status.timeout >= 3600) {
return tr::lng_code_call(
tr::now,
lt_minutes,
(u"%1:%2"_q)
.arg(_status.timeout / 3600)
.arg((_status.timeout / 60) % 60, 2, 10, QChar('0')),
lt_seconds,
(u"%1"_q).arg(_status.timeout % 60, 2, 10, QChar('0')));
}
return tr::lng_code_call(
tr::now,
lt_minutes,
QString::number(_status.timeout / 60),
lt_seconds,
(u"%1"_q).arg(_status.timeout % 60, 2, 10, QChar('0')));
} break;
case State::Calling: return tr::lng_code_calling(tr::now);
case State::Called: return tr::lng_code_called(tr::now);
}
return QString();
}
void SentCodeCall::callDone() {
if (_status.state == State::Calling) {
_status.state = State::Called;
if (_update) {
_update();
}
}
}
} // namespace Ui

View File

@@ -0,0 +1,74 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
#include "base/timer.h"
#include "ui/widgets/fields/input_field.h"
namespace Ui {
class SentCodeField final : public Ui::InputField {
public:
SentCodeField(
QWidget *parent,
const style::InputField &st,
rpl::producer<QString> placeholder = nullptr,
const QString &val = QString());
void setAutoSubmit(int length, Fn<void()> submitCallback);
void setChangedCallback(Fn<void()> changedCallback);
[[nodiscard]] QString getDigitsOnly() const;
private:
void fix();
// Flag for not calling onTextChanged() recursively.
bool _fixing = false;
int _autoSubmitLength = 0;
Fn<void()> _submitCallback;
Fn<void()> _changedCallback;
};
class SentCodeCall final {
public:
SentCodeCall(
FnMut<void()> callCallback,
Fn<void()> updateCallback);
enum class State {
Waiting,
Calling,
Called,
Disabled,
};
struct Status {
Status() {
}
Status(State state, int timeout) : state(state), timeout(timeout) {
}
State state = State::Disabled;
int timeout = 0;
};
void setStatus(const Status &status);
void callDone();
[[nodiscard]] QString getText() const;
private:
Status _status;
base::Timer _timer;
FnMut<void()> _call;
Fn<void()> _update;
};
} // namespace Ui

View File

@@ -0,0 +1,21 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
#include "ui/widgets/discrete_sliders.h"
namespace Ui {
class CustomWidthSlider final : public SettingsSlider {
public:
using Ui::SettingsSlider::SettingsSlider;
using SettingsSlider::setNaturalWidth;
};
} // namespace Ui

View File

@@ -0,0 +1,320 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "ui/widgets/vertical_drum_picker.h"
#include "ui/effects/animation_value_f.h"
#include "ui/ui_utility.h"
#include "styles/style_basic.h"
#include "styles/style_widgets.h"
namespace Ui {
namespace {
constexpr auto kAlmostIndex = float64(.99);
constexpr auto kMinYScale = 0.2;
using PaintItemCallback = VerticalDrumPicker::PaintItemCallback;
} // namespace
PaintItemCallback VerticalDrumPicker::DefaultPaintCallback(
const style::font &font,
int itemHeight,
Fn<void(QPainter&, QRectF, int)> paintContent) {
return [=](
QPainter &p,
int index,
float64 y,
float64 distanceFromCenter,
int outerWidth) {
const auto r = QRectF(0, y, outerWidth, itemHeight);
const auto progress = std::abs(distanceFromCenter);
const auto revProgress = 1. - progress;
p.save();
p.translate(r.center());
const auto yScale = kMinYScale
+ (1. - kMinYScale) * anim::easeOutCubic(1., revProgress);
p.scale(1., yScale);
p.translate(-r.center());
p.setOpacity(revProgress);
p.setFont(font);
p.setPen(st::defaultFlatLabel.textFg);
paintContent(p, r, index);
p.restore();
};
}
PickerAnimation::PickerAnimation() = default;
void PickerAnimation::jumpToOffset(int offset) {
_result.from = _result.current;
_result.to += offset;
_animation.stop();
auto callback = [=](float64 value) {
const auto was = _result.current;
_result.current = anim::interpolateF(
_result.from,
_result.to,
value);
_updates.fire(_result.current - was);
};
if (anim::Disabled()) {
auto value = float64(0.);
const auto diff = _result.to - _result.from;
const auto step = std::min(
kAlmostIndex,
1. / (std::max(1. - kAlmostIndex, std::abs(diff) + 1)));
while (true) {
value += step;
if (value >= 1.) {
callback(1.);
break;
} else {
callback(value);
}
}
return;
}
_animation.start(
std::move(callback),
0.,
1.,
st::fadeWrapDuration);
}
void PickerAnimation::setResult(float64 from, float64 current, float64 to) {
_result = { from, current, to };
}
rpl::producer<PickerAnimation::Shift> PickerAnimation::updates() const {
return _updates.events();
}
VerticalDrumPicker::VerticalDrumPicker(
not_null<Ui::RpWidget*> parent,
PaintItemCallback &&paintCallback,
int itemsCount,
int itemHeight,
int startIndex,
bool looped)
: RpWidget(parent)
, _itemsCount(itemsCount)
, _itemHeight(itemHeight)
, _paintCallback(std::move(paintCallback))
, _pendingStartIndex(startIndex)
, _loopData({ .looped = looped }) {
Expects(_paintCallback != nullptr);
sizeValue(
) | rpl::on_next([=](const QSize &s) {
_itemsVisible.count = std::ceil(float64(s.height()) / _itemHeight);
_itemsVisible.centerOffset = _itemsVisible.count / 2;
if ((_pendingStartIndex >= 0) && _itemsVisible.count) {
_index = normalizedIndex(_pendingStartIndex
- _itemsVisible.centerOffset);
_pendingStartIndex = -1;
}
if (!_loopData.looped) {
_loopData.minIndex = -_itemsVisible.centerOffset;
_loopData.maxIndex = _itemsCount - 1 - _itemsVisible.centerOffset;
}
_changes.fire({});
}, lifetime());
paintRequest(
) | rpl::on_next([=] {
auto p = QPainter(this);
const auto outerWidth = width();
const auto centerY = height() / 2.;
const auto shiftedY = _itemHeight * _shift;
for (auto i = -1; i < (_itemsVisible.count + 1); i++) {
const auto index = normalizedIndex(i + _index);
if (!isIndexInRange(index)) {
continue;
}
const auto y = (_itemHeight * i + shiftedY);
_paintCallback(
p,
index,
y,
((y + _itemHeight / 2.) - centerY) / centerY,
outerWidth);
}
}, lifetime());
_animation.updates(
) | rpl::on_next([=](PickerAnimation::Shift shift) {
increaseShift(shift);
}, lifetime());
}
void VerticalDrumPicker::increaseShift(float64 by) {
{
// Guard input.
if (by >= 1.) {
by = kAlmostIndex;
} else if (by <= -1.) {
by = -kAlmostIndex;
}
}
auto shift = _shift;
auto index = _index;
shift += by;
if (shift >= 1.) {
shift -= 1.;
index--;
index = normalizedIndex(index);
} else if (shift <= -1.) {
shift += 1.;
index++;
index = normalizedIndex(index);
}
if (_loopData.minIndex == _loopData.maxIndex) {
_shift = 0.;
} else if (!_loopData.looped && (index <= _loopData.minIndex)) {
_shift = std::min(0., shift);
_index = _loopData.minIndex;
} else if (!_loopData.looped && (index >= _loopData.maxIndex)) {
_shift = std::max(0., shift);
_index = _loopData.maxIndex;
} else {
_shift = shift;
_index = index;
}
_changes.fire({});
update();
}
void VerticalDrumPicker::handleWheelEvent(not_null<QWheelEvent*> e) {
const auto direction = Ui::WheelDirection(e);
if (direction) {
_animation.jumpToOffset(direction);
} else {
if (const auto delta = e->pixelDelta().y(); delta) {
increaseShift(delta / float64(_itemHeight));
} else if (e->phase() == Qt::ScrollEnd) {
animationDataFromIndex();
_animation.jumpToOffset(0);
} else {
constexpr auto step = int(QWheelEvent::DefaultDeltasPerStep);
_touch.verticalDelta += e->angleDelta().y();
while (std::abs(_touch.verticalDelta) >= step) {
if (_touch.verticalDelta < 0) {
_touch.verticalDelta += step;
_animation.jumpToOffset(1);
} else {
_touch.verticalDelta -= step;
_animation.jumpToOffset(-1);
}
}
}
}
}
void VerticalDrumPicker::handleKeyEvent(not_null<QKeyEvent*> e) {
if (e->key() == Qt::Key_Left || e->key() == Qt::Key_Up) {
_animation.jumpToOffset(1);
} else if (e->key() == Qt::Key_PageUp && !e->isAutoRepeat()) {
_animation.jumpToOffset(_itemsVisible.count);
} else if (e->key() == Qt::Key_Right || e->key() == Qt::Key_Down) {
_animation.jumpToOffset(-1);
} else if (e->key() == Qt::Key_PageDown && !e->isAutoRepeat()) {
_animation.jumpToOffset(-_itemsVisible.count);
}
}
void VerticalDrumPicker::handleMouseEvent(not_null<QMouseEvent*> e) {
if (e->type() == QEvent::MouseButtonPress) {
_mouse.pressed = true;
_mouse.lastPositionY = e->pos().y();
} else if (e->type() == QEvent::MouseMove) {
if (_mouse.pressed) {
const auto was = _mouse.lastPositionY;
_mouse.lastPositionY = e->pos().y();
const auto diff = _mouse.lastPositionY - was;
increaseShift(float64(diff) / _itemHeight);
_mouse.clickDisabled = true;
}
} else if (e->type() == QEvent::MouseButtonRelease) {
if (_mouse.clickDisabled) {
animationDataFromIndex();
_animation.jumpToOffset(0);
} else {
_mouse.lastPositionY = e->pos().y();
const auto toOffset = _itemsVisible.centerOffset
- (_mouse.lastPositionY / _itemHeight);
_animation.jumpToOffset(toOffset);
}
_mouse = {};
}
}
void VerticalDrumPicker::wheelEvent(QWheelEvent *e) {
handleWheelEvent(e);
}
void VerticalDrumPicker::mousePressEvent(QMouseEvent *e) {
handleMouseEvent(e);
}
void VerticalDrumPicker::mouseMoveEvent(QMouseEvent *e) {
handleMouseEvent(e);
}
void VerticalDrumPicker::mouseReleaseEvent(QMouseEvent *e) {
handleMouseEvent(e);
}
void VerticalDrumPicker::keyPressEvent(QKeyEvent *e) {
handleKeyEvent(e);
}
void VerticalDrumPicker::animationDataFromIndex() {
_animation.setResult(
_index,
_index + _shift,
std::round(_index + _shift));
}
bool VerticalDrumPicker::isIndexInRange(int index) const {
return (index >= 0) && (index < _itemsCount);
}
int VerticalDrumPicker::normalizedIndex(int index) const {
if (!_loopData.looped) {
return index;
}
if (index < 0) {
index += _itemsCount;
} else if (index >= _itemsCount) {
index -= _itemsCount;
}
return index;
}
int VerticalDrumPicker::index() const {
return normalizedIndex(_index + _itemsVisible.centerOffset);
}
rpl::producer<int> VerticalDrumPicker::changes() const {
return _changes.events() | rpl::map([=] { return index(); });
}
rpl::producer<int> VerticalDrumPicker::value() const {
return rpl::single(index())
| rpl::then(changes())
| rpl::distinct_until_changed();
}
} // namespace Ui

View File

@@ -0,0 +1,116 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
#include "ui/rp_widget.h"
#include "ui/effects/animations.h"
namespace Ui {
class PickerAnimation final {
public:
using Shift = float64;
PickerAnimation();
void jumpToOffset(int offset);
void setResult(float64 from, float64 current, float64 to);
[[nodiscard]] rpl::producer<Shift> updates() const;
private:
Ui::Animations::Simple _animation;
struct {
float64 from = 0.;
float64 current = 0.;
float64 to = 0.;
} _result;
rpl::event_stream<Shift> _updates;
};
class VerticalDrumPicker final : public Ui::RpWidget {
public:
using PaintItemCallback = Fn<void(
QPainter &p,
int index,
float y,
float64 distanceFromCenter,
int outerWidth)>;
VerticalDrumPicker(
not_null<Ui::RpWidget*> parent,
PaintItemCallback &&paintCallback,
int itemsCount,
int itemHeight,
int startIndex = 0,
bool looped = false);
[[nodiscard]] int index() const;
[[nodiscard]] rpl::producer<int> changes() const;
[[nodiscard]] rpl::producer<int> value() const;
void handleWheelEvent(not_null<QWheelEvent*> e);
void handleMouseEvent(not_null<QMouseEvent*> e);
void handleKeyEvent(not_null<QKeyEvent*> e);
static PaintItemCallback DefaultPaintCallback(
const style::font &font,
int itemHeight,
Fn<void(QPainter&, QRectF, int)> paintContent);
protected:
void wheelEvent(QWheelEvent *e) override;
void mousePressEvent(QMouseEvent *e) override;
void mouseMoveEvent(QMouseEvent *e) override;
void mouseReleaseEvent(QMouseEvent *e) override;
void keyPressEvent(QKeyEvent *e) override;
private:
void increaseShift(float64 by);
void animationDataFromIndex();
[[nodiscard]] int normalizedIndex(int index) const;
[[nodiscard]] bool isIndexInRange(int index) const;
const int _itemsCount;
const int _itemHeight;
PaintItemCallback _paintCallback;
int _pendingStartIndex = -1;
struct {
int count = 0;
int centerOffset = 0;
} _itemsVisible;
int _index = 0;
float64 _shift = 0.;
rpl::event_stream<> _changes;
struct {
const bool looped;
int minIndex = 0;
int maxIndex = 0;
} _loopData;
PickerAnimation _animation;
struct {
bool pressed = false;
int lastPositionY;
bool clickDisabled = false;
} _mouse;
struct {
int verticalDelta = 0;
} _touch;
};
} // namespace Ui