Implement better horizontal/vertical tabs.

This commit is contained in:
John Preston 2025-05-27 13:54:56 +04:00
parent 0e5419c60b
commit 8512154b45
14 changed files with 908 additions and 523 deletions

View file

@ -93,11 +93,12 @@ void DefaultIconEmoji::paint(QPainter &p, const Context &context) {
const auto &st = (_tag == Data::CustomEmojiSizeTag::Normal)
? st::normalForumTopicIcon
: st::defaultForumTopicIcon;
const auto general = Data::IsForumGeneralIconTitle(_icon.title);
if (_image.isNull()) {
_image = Data::IsForumGeneralIconTitle(_icon.title)
_image = general
? Data::ForumTopicGeneralIconFrame(
st.size,
Data::ParseForumGeneralIconColor(_icon.colorId))
QColor(255, 255, 255))
: Data::ForumTopicIconFrame(_icon.colorId, _icon.title, st);
}
const auto full = (_tag == Data::CustomEmojiSizeTag::Normal)
@ -106,7 +107,9 @@ void DefaultIconEmoji::paint(QPainter &p, const Context &context) {
const auto esize = full / style::DevicePixelRatio();
const auto customSize = Ui::Text::AdjustCustomEmojiSize(esize);
const auto skip = (customSize - st.size) / 2;
p.drawImage(context.position + QPoint(skip, skip), _image);
p.drawImage(context.position + QPoint(skip, skip), general
? style::colorizeImage(_image, context.textColor)
: _image);
}
void DefaultIconEmoji::unload() {

View file

@ -408,6 +408,11 @@ void ChannelData::setPendingRequestsCount(
}
}
bool ChannelData::useSubsectionTabs() const {
return isForum()
&& ((flags() & ChannelDataFlag::ForumTabs) || true); AssertIsDebug();
}
ChatRestrictionsInfo ChannelData::KickedRestrictedRights(
not_null<PeerData*> participant) {
using Flag = ChatRestriction;

View file

@ -279,6 +279,7 @@ public:
[[nodiscard]] bool paidMessagesAvailable() const {
return flags() & Flag::PaidMessagesAvailable;
}
[[nodiscard]] bool useSubsectionTabs() const;
[[nodiscard]] static ChatRestrictionsInfo KickedRestrictedRights(
not_null<PeerData*> participant);

View file

@ -152,10 +152,10 @@ QImage ForumTopicGeneralIconFrame(int size, const QColor &color) {
result.setDevicePixelRatio(ratio);
result.fill(Qt::transparent);
const auto use = size * 0.8;
const auto skip = size * 0.1;
const auto use = size * 1.;
const auto skip = size * 0.;
auto p = QPainter(&result);
svg.render(&p, QRectF(skip, 0, use, use));
svg.render(&p, QRectF(skip, skip, use, use));
p.end();
return style::colorizeImage(result, color);

View file

@ -500,8 +500,8 @@ dialogsLoadMoreLoading: InfiniteRadialAnimation(defaultInfiniteRadialAnimation)
}
dialogsSearchInHeight: 38px;
dialogsSearchInPhotoSize: 26px;
dialogsSearchInPhotoPadding: 12px;
dialogsSearchInPhotoSize: 28px;
dialogsSearchInPhotoPadding: 10px;
dialogsSearchInSkip: 10px;
dialogsSearchInNameTop: 9px;
dialogsSearchInDownTop: 15px;

View file

@ -4218,12 +4218,20 @@ void InnerWidget::updateSearchIn() {
: _openedForum
? _openedForum->channel().get()
: nullptr;
const auto paused = [window = _controller] {
return window->isGifPausedAtLeastFor(Window::GifPauseReason::Any);
};
const auto textFg = [] {
return st::windowSubTextFg->c;
};
const auto topicIcon = !topic
? nullptr
: topic->iconId()
? Ui::MakeEmojiThumbnail(
&topic->owner(),
Data::SerializeCustomEmojiId(topic->iconId()))
Data::SerializeCustomEmojiId(topic->iconId()),
paused,
textFg)
: Ui::MakeEmojiThumbnail(
&topic->owner(),
Data::TopicIconEmojiEntity({
@ -4233,7 +4241,9 @@ void InnerWidget::updateSearchIn() {
.colorId = (topic->isGeneral()
? Data::ForumGeneralIconColor(st::windowSubTextFg->c)
: topic->colorId()),
}));
}),
paused,
textFg);
const auto peerIcon = peer
? Ui::MakeUserpicThumbnail(peer)
: sublist

View file

@ -21,6 +21,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "history/history.h"
#include "lang/lang_keys.h"
#include "main/main_session.h"
#include "ui/controls/subsection_tabs_slider.h"
#include "ui/effects/ripple_animation.h"
#include "ui/text/text_utilities.h"
#include "ui/widgets/buttons.h"
@ -36,284 +37,6 @@ namespace HistoryView {
namespace {
constexpr auto kDefaultLimit = 5; AssertIsDebug()// 10;
constexpr auto kMaxNameLines = 3;
class VerticalSlider final : public Ui::RpWidget {
public:
explicit VerticalSlider(not_null<QWidget*> parent);
struct Section {
std::shared_ptr<Ui::DynamicImage> userpic;
QString text;
};
void setSections(std::vector<Section> sections, Fn<bool()> paused);
void setActiveSectionFast(int active);
void fitHeightToSections();
[[nodiscard]] rpl::producer<int> sectionActivated() const {
return _sectionActivated.events();
}
[[nodiscard]] int sectionsCount() const;
[[nodiscard]] int lookupSectionTop(int index) const;
private:
struct Tab {
std::shared_ptr<Ui::DynamicImage> userpic;
Ui::Text::String text;
std::unique_ptr<Ui::RippleAnimation> ripple;
int top = 0;
int height = 0;
bool subscribed = false;
};
struct Range {
int top = 0;
int height = 0;
};
void paintEvent(QPaintEvent *e) override;
void timerEvent(QTimerEvent *e) override;
void mousePressEvent(QMouseEvent *e) override;
void mouseReleaseEvent(QMouseEvent *e) override;
void startRipple(int index);
[[nodiscard]] int getIndexFromPosition(QPoint position) const;
[[nodiscard]] QImage prepareRippleMask(int index, const Tab &tab);
void activateCallback();
[[nodiscard]] Range getFinalActiveRange() const;
const style::ChatTabsVertical &_st;
Ui::RoundRect _bar;
std::vector<Tab> _tabs;
int _active = -1;
int _pressed = -1;
Ui::Animations::Simple _activeTop;
Ui::Animations::Simple _activeHeight;
int _timerId = -1;
crl::time _callbackAfterMs = 0;
rpl::event_stream<int> _sectionActivated;
Fn<bool()> _paused;
};
VerticalSlider::VerticalSlider(not_null<QWidget*> parent)
: RpWidget(parent)
, _st(st::chatTabsVertical)
, _bar(_st.barRadius, _st.barFg) {
setCursor(style::cur_pointer);
}
void VerticalSlider::setSections(
std::vector<Section> sections,
Fn<bool()> paused) {
auto old = base::take(_tabs);
_tabs.reserve(sections.size());
for (auto &section : sections) {
const auto i = ranges::find(old, section.userpic, &Tab::userpic);
if (i != end(old)) {
_tabs.push_back(std::move(*i));
old.erase(i);
} else {
_tabs.push_back({ .userpic = std::move(section.userpic), });
}
_tabs.back().text = Ui::Text::String(
_st.nameStyle,
section.text,
kDefaultTextOptions,
_st.nameWidth);
}
for (const auto &was : old) {
if (was.subscribed) {
was.userpic->subscribeToUpdates(nullptr);
}
}
}
void VerticalSlider::setActiveSectionFast(int active) {
_active = active;
_activeTop.stop();
_activeHeight.stop();
}
void VerticalSlider::fitHeightToSections() {
auto top = 0;
for (auto &tab : _tabs) {
tab.top = top;
tab.height = _st.baseHeight + std::min(
_st.nameStyle.font->height * kMaxNameLines,
tab.text.countHeight(_st.nameWidth, true));
top += tab.height;
}
resize(_st.width, top);
}
int VerticalSlider::sectionsCount() const {
return int(_tabs.size());
}
int VerticalSlider::lookupSectionTop(int index) const {
Expects(index >= 0 && index < _tabs.size());
return _tabs[index].top;
}
VerticalSlider::Range VerticalSlider::getFinalActiveRange() const {
return (_active >= 0)
? Range{ _tabs[_active].top, _tabs[_active].height }
: Range();
}
void VerticalSlider::paintEvent(QPaintEvent *e) {
const auto finalRange = getFinalActiveRange();
const auto range = Range{
int(base::SafeRound(_activeTop.value(finalRange.top))),
int(base::SafeRound(_activeHeight.value(finalRange.height))),
};
auto p = QPainter(this);
auto clip = e->rect();
const auto drawRect = [&](QRect rect) {
_bar.paint(p, rect);
};
const auto nameLeft = (_st.width - _st.nameWidth) / 2;
for (auto &tab : _tabs) {
if (!clip.intersects(QRect(0, tab.top, width(), tab.height))) {
continue;
}
const auto divider = std::max(std::min(tab.height, range.height), 1);
const auto active = 1.
- std::clamp(
std::abs(range.top - tab.top) / float64(divider),
0.,
1.);
if (tab.ripple) {
const auto color = anim::color(
_st.rippleBg,
_st.rippleBgActive,
active);
tab.ripple->paint(p, 0, tab.top, width(), &color);
if (tab.ripple->empty()) {
tab.ripple.reset();
}
}
if (!tab.subscribed) {
tab.subscribed = true;
tab.userpic->subscribeToUpdates([=] { update(); });
}
const auto &image = tab.userpic->image(_st.userpicSize);
const auto userpicLeft = (width() - _st.userpicSize) / 2;
p.drawImage(userpicLeft, tab.top + _st.userpicTop, image);
p.setPen(anim::pen(_st.nameFg, _st.nameFgActive, active));
tab.text.draw(p, {
.position = QPoint(nameLeft, tab.top + _st.nameTop),
.outerWidth = width(),
.availableWidth = _st.nameWidth,
.align = style::al_top,
.paused = _paused && _paused(),
});
}
if (range.height > 0) {
const auto add = _st.barStroke / 2;
drawRect(myrtlrect(-add, range.top, _st.barStroke, range.height));
}
}
void VerticalSlider::timerEvent(QTimerEvent *e) {
activateCallback();
}
void VerticalSlider::startRipple(int index) {
if (!_st.ripple.showDuration) {
return;
}
auto &tab = _tabs[index];
if (!tab.ripple) {
auto mask = prepareRippleMask(index, tab);
tab.ripple = std::make_unique<Ui::RippleAnimation>(
_st.ripple,
std::move(mask),
[this] { update(); });
}
const auto point = mapFromGlobal(QCursor::pos());
tab.ripple->add(point - QPoint(0, tab.top));
}
QImage VerticalSlider::prepareRippleMask(int index, const Tab &tab) {
return Ui::RippleAnimation::RectMask(QSize(width(), tab.height));
}
int VerticalSlider::getIndexFromPosition(QPoint position) const {
const auto count = int(_tabs.size());
for (auto i = 0; i != count; ++i) {
const auto &tab = _tabs[i];
if (position.y() < tab.top + tab.height) {
return i;
}
}
return count - 1;
}
void VerticalSlider::mousePressEvent(QMouseEvent *e) {
for (auto i = 0, count = int(_tabs.size()); i != count; ++i) {
auto &tab = _tabs[i];
if (tab.top <= e->y() && e->y() < tab.top + tab.height) {
startRipple(i);
_pressed = i;
break;
}
}
}
void VerticalSlider::mouseReleaseEvent(QMouseEvent *e) {
const auto pressed = std::exchange(_pressed, -1);
if (pressed < 0) {
return;
}
const auto index = getIndexFromPosition(e->pos());
if (pressed < _tabs.size()) {
if (_tabs[pressed].ripple) {
_tabs[pressed].ripple->lastStop();
}
}
if (index == pressed) {
if (_active != index) {
_callbackAfterMs = crl::now() + _st.duration;
activateCallback();
const auto from = getFinalActiveRange();
_active = index;
const auto to = getFinalActiveRange();
const auto updater = [this] { update(); };
_activeTop.start(updater, from.top, to.top, _st.duration);
_activeHeight.start(
updater,
from.height,
to.height,
_st.duration);
}
}
}
void VerticalSlider::activateCallback() {
if (_timerId >= 0) {
killTimer(_timerId);
_timerId = -1;
}
auto ms = crl::now();
if (ms >= _callbackAfterMs) {
_sectionActivated.fire_copy(_active);
} else {
_timerId = startTimer(_callbackAfterMs - ms, Qt::PreciseTimer);
}
}
} // namespace
@ -370,18 +93,13 @@ void SubsectionTabs::setupHorizontal(not_null<QWidget*> parent) {
st::chatTabsScroll,
true);
scroll->show();
const auto tabs = scroll->setOwnedWidget(
object_ptr<Ui::SettingsSlider>(scroll, st::chatTabsSlider));
tabs->sectionActivated() | rpl::start_with_next([=](int active) {
if (active >= 0
&& active < _slice.size()
&& _active != _slice[active]) {
auto params = Window::SectionShow();
params.way = Window::SectionShow::Way::ClearStack;
params.animated = anim::type::instant;
_controller->showThread(_slice[active], {}, params);
}
}, tabs->lifetime());
const auto slider = scroll->setOwnedWidget(
object_ptr<Ui::HorizontalSlider>(scroll));
setupSlider(scroll, slider, false);
_horizontal->resize(
_horizontal->width(),
std::max(toggle->height(), slider->height()));
scroll->setCustomWheelProcess([=](not_null<QWheelEvent*> e) {
const auto pixelDelta = e->pixelDelta();
@ -394,36 +112,6 @@ void SubsectionTabs::setupHorizontal(not_null<QWidget*> parent) {
return true;
});
rpl::merge(
scroll->scrolls(),
_scrollCheckRequests.events(),
scroll->widthValue() | rpl::skip(1) | rpl::map_to(rpl::empty)
) | rpl::start_with_next([=] {
const auto width = scroll->width();
const auto left = scroll->scrollLeft();
const auto max = scroll->scrollLeftMax();
const auto availableLeft = left;
const auto availableRight = (max - left);
if (max <= 2 * width && _afterAvailable > 0) {
_beforeLimit *= 2;
_afterLimit *= 2;
}
if (availableLeft < width
&& _beforeSkipped.value_or(0) > 0
&& !_slice.empty()) {
_around = _slice.front();
refreshSlice();
} else if (availableRight < width) {
if (_afterAvailable > 0) {
_around = _slice.back();
refreshSlice();
} else if (!_afterSkipped.has_value()) {
_loading = true;
loadMore();
}
}
}, _horizontal->lifetime());
_horizontal->sizeValue(
) | rpl::start_with_next([=](QSize size) {
const auto togglew = toggle->width();
@ -438,89 +126,6 @@ void SubsectionTabs::setupHorizontal(not_null<QWidget*> parent) {
{ 0, 0, 0, st::lineWidth })),
st::windowBg);
}, _horizontal->lifetime());
_refreshed.events_starting_with_copy(
rpl::empty
) | rpl::start_with_next([=] {
auto sections = std::vector<TextWithEntities>();
const auto manager = &_history->owner().customEmojiManager();
auto activeIndex = -1;
for (const auto &thread : _slice) {
if (thread == _active) {
activeIndex = int(sections.size());
}
if (const auto topic = thread->asTopic()) {
sections.push_back(topic->titleWithIcon());
} else if (const auto sublist = thread->asSublist()) {
const auto peer = sublist->sublistPeer();
sections.push_back(TextWithEntities().append(
Ui::Text::SingleCustomEmoji(
manager->peerUserpicEmojiData(peer),
u"@"_q)
).append(' ').append(peer->shortName()));
} else {
sections.push_back(tr::lng_filters_all_short(
tr::now,
Ui::Text::WithEntities));
}
}
const auto paused = [=] {
return _controller->isGifPausedAtLeastFor(
Window::GifPauseReason::Any);
};
auto scrollSavingThread = (Data::Thread*)nullptr;
auto scrollSavingShift = 0;
auto scrollSavingIndex = -1;
if (const auto count = tabs->sectionsCount()) {
const auto scrollLeft = scroll->scrollLeft();
auto indexLeft = tabs->lookupSectionLeft(0);
for (auto index = 0; index != count; ++index) {
const auto nextLeft = (index + 1 != count)
? tabs->lookupSectionLeft(index + 1)
: (indexLeft + scrollLeft + 1);
if (indexLeft <= scrollLeft && nextLeft > scrollLeft) {
scrollSavingThread = _sectionsSlice[index];
scrollSavingShift = scrollLeft - indexLeft;
break;
}
indexLeft = nextLeft;
}
scrollSavingIndex = scrollSavingThread
? int(ranges::find(_slice, not_null(scrollSavingThread))
- begin(_slice))
: -1;
if (scrollSavingIndex == _slice.size()) {
scrollSavingIndex = -1;
for (auto index = 0; index != count; ++index) {
const auto thread = _sectionsSlice[index];
if (ranges::contains(_slice, thread)) {
scrollSavingThread = thread;
scrollSavingShift = scrollLeft
- tabs->lookupSectionLeft(index);
scrollSavingIndex = index;
break;
}
}
}
}
tabs->setSections(sections, Core::TextContext({
.session = &_history->session(),
}), paused);
tabs->fitWidthToSections();
tabs->setActiveSectionFast(activeIndex);
_sectionsSlice = _slice;
_horizontal->resize(
_horizontal->width(),
std::max(toggle->height(), tabs->height()));
if (scrollSavingIndex >= 0) {
scroll->scrollToX(tabs->lookupSectionLeft(scrollSavingIndex)
+ scrollSavingShift);
}
_scrollCheckRequests.fire({});
}, _horizontal->lifetime());
}
void SubsectionTabs::setupVertical(not_null<QWidget*> parent) {
@ -547,48 +152,14 @@ void SubsectionTabs::setupVertical(not_null<QWidget*> parent) {
_vertical,
st::chatTabsScroll);
scroll->show();
const auto tabs = scroll->setOwnedWidget(
object_ptr<VerticalSlider>(scroll));
tabs->sectionActivated() | rpl::start_with_next([=](int active) {
if (active >= 0
&& active < _slice.size()
&& _active != _slice[active]) {
auto params = Window::SectionShow();
params.way = Window::SectionShow::Way::ClearStack;
params.animated = anim::type::instant;
_controller->showThread(_slice[active], {}, params);
}
}, tabs->lifetime());
rpl::merge(
scroll->scrolls(),
_scrollCheckRequests.events(),
scroll->heightValue() | rpl::skip(1) | rpl::map_to(rpl::empty)
) | rpl::start_with_next([=] {
const auto height = scroll->height();
const auto top = scroll->scrollTop();
const auto max = scroll->scrollTopMax();
const auto availableTop = top;
const auto availableBottom = (max - top);
if (max <= 2 * height && _afterAvailable > 0) {
_beforeLimit *= 2;
_afterLimit *= 2;
}
if (availableTop < height
&& _beforeSkipped.value_or(0) > 0
&& !_slice.empty()) {
_around = _slice.front();
refreshSlice();
} else if (availableBottom < height) {
if (_afterAvailable > 0) {
_around = _slice.back();
refreshSlice();
} else if (!_afterSkipped.has_value()) {
_loading = true;
loadMore();
}
}
}, _vertical->lifetime());
const auto slider = scroll->setOwnedWidget(
object_ptr<Ui::VerticalSlider>(scroll));
setupSlider(scroll, slider, true);
_vertical->resize(
std::max(toggle->width(), slider->width()),
_vertical->height());
_vertical->sizeValue(
) | rpl::start_with_next([=](QSize size) {
@ -600,61 +171,149 @@ void SubsectionTabs::setupVertical(not_null<QWidget*> parent) {
_vertical->paintRequest() | rpl::start_with_next([=](QRect clip) {
QPainter(_vertical).fillRect(clip, st::windowBg);
}, _vertical->lifetime());
}
void SubsectionTabs::setupSlider(
not_null<Ui::ScrollArea*> scroll,
not_null<Ui::SubsectionSlider*> slider,
bool vertical) {
slider->sectionActivated() | rpl::start_with_next([=](int active) {
if (active >= 0
&& active < _slice.size()
&& _active != _slice[active]) {
auto params = Window::SectionShow();
params.way = Window::SectionShow::Way::ClearStack;
params.animated = anim::type::instant;
_controller->showThread(_slice[active], {}, params);
}
}, slider->lifetime());
rpl::merge(
scroll->scrolls(),
_scrollCheckRequests.events(),
scroll->heightValue() | rpl::skip(1) | rpl::map_to(rpl::empty)
) | rpl::start_with_next([=] {
const auto full = vertical ? scroll->height() : scroll->width();
const auto scrollValue = vertical
? scroll->scrollTop()
: scroll->scrollLeft();
const auto scrollMax = vertical
? scroll->scrollTopMax()
: scroll->scrollLeftMax();
const auto availableFrom = scrollValue;
const auto availableTill = (scrollMax - scrollValue);
if (scrollMax <= 2 * full && _afterAvailable > 0) {
_beforeLimit *= 2;
_afterLimit *= 2;
}
if (availableFrom < full
&& _beforeSkipped.value_or(0) > 0
&& !_slice.empty()) {
_around = _slice.front();
refreshSlice();
} else if (availableTill < full) {
if (_afterAvailable > 0) {
_around = _slice.back();
refreshSlice();
} else if (!_afterSkipped.has_value()) {
_loading = true;
loadMore();
}
}
}, scroll->lifetime());
_refreshed.events_starting_with_copy(
rpl::empty
) | rpl::start_with_next([=] {
auto sections = std::vector<VerticalSlider::Section>();
auto activeIndex = -1;
for (const auto &thread : _slice) {
if (thread == _active) {
activeIndex = int(sections.size());
}
if (const auto topic = thread->asTopic()) {
sections.push_back({
.userpic = (topic->iconId()
? Ui::MakeEmojiThumbnail(
&topic->owner(),
Data::SerializeCustomEmojiId(topic->iconId()))
: Ui::MakeUserpicThumbnail(
_controller->session().user())),
.text = topic->title(),
});
} else if (const auto sublist = thread->asSublist()) {
const auto peer = sublist->sublistPeer();
sections.push_back({
.userpic = Ui::MakeUserpicThumbnail(peer),
.text = peer->shortName(),
});
} else {
sections.push_back({
.userpic = Ui::MakeUserpicThumbnail(
_controller->session().user()),
.text = tr::lng_filters_all_short(tr::now),
});
}
}
const auto manager = &_history->owner().customEmojiManager();
const auto paused = [=] {
return _controller->isGifPausedAtLeastFor(
Window::GifPauseReason::Any);
};
auto sections = std::vector<Ui::SubsectionTab>();
auto activeIndex = -1;
for (const auto &thread : _slice) {
const auto index = int(sections.size());
if (thread == _active) {
activeIndex = index;
}
const auto textFg = [=] {
return anim::color(
st::windowSubTextFg,
st::windowActiveTextFg,
slider->buttonActive(slider->buttonAt(index)));
};
if (const auto topic = thread->asTopic()) {
if (vertical) {
sections.push_back({
.text = { topic->title() },
.userpic = (topic->iconId()
? Ui::MakeEmojiThumbnail(
&topic->owner(),
Data::SerializeCustomEmojiId(topic->iconId()),
paused,
textFg)
: Ui::MakeEmojiThumbnail(
&topic->owner(),
Data::TopicIconEmojiEntity({
.title = (topic->isGeneral()
? Data::ForumGeneralIconTitle()
: topic->title()),
.colorId = (topic->isGeneral()
? Data::ForumGeneralIconColor(
st::windowSubTextFg->c)
: topic->colorId()),
}),
paused,
textFg)),
});
} else {
sections.push_back({
.text = topic->titleWithIcon(),
});
}
} else if (const auto sublist = thread->asSublist()) {
const auto peer = sublist->sublistPeer();
if (vertical) {
sections.push_back({
.text = peer->shortName(),
.userpic = Ui::MakeUserpicThumbnail(peer),
});
} else {
sections.push_back({
.text = TextWithEntities().append(
Ui::Text::SingleCustomEmoji(
manager->peerUserpicEmojiData(peer),
u"@"_q)
).append(' ').append(peer->shortName()),
});
}
} else {
sections.push_back({
.text = tr::lng_filters_all_short(tr::now),
.userpic = Ui::MakeAllSubsectionsThumbnail(textFg),
});
}
}
auto scrollSavingThread = (Data::Thread*)nullptr;
auto scrollSavingShift = 0;
auto scrollSavingIndex = -1;
if (const auto count = tabs->sectionsCount()) {
const auto scrollTop = scroll->scrollTop();
auto indexTop = tabs->lookupSectionTop(0);
if (const auto count = slider->sectionsCount()) {
const auto scrollValue = vertical
? scroll->scrollTop()
: scroll->scrollLeft();
auto indexPosition = slider->lookupSectionPosition(0);
for (auto index = 0; index != count; ++index) {
const auto nextTop = (index + 1 != count)
? tabs->lookupSectionTop(index + 1)
: (indexTop + scrollTop + 1);
if (indexTop <= scrollTop && nextTop > scrollTop) {
const auto nextPosition = (index + 1 != count)
? slider->lookupSectionPosition(index + 1)
: (indexPosition + scrollValue + 1);
if (indexPosition <= scrollValue && nextPosition > scrollValue) {
scrollSavingThread = _sectionsSlice[index];
scrollSavingShift = scrollTop - indexTop;
scrollSavingShift = scrollValue - indexPosition;
break;
}
indexTop = nextTop;
indexPosition = nextPosition;
}
scrollSavingIndex = scrollSavingThread
? int(ranges::find(_slice, not_null(scrollSavingThread))
@ -666,8 +325,8 @@ void SubsectionTabs::setupVertical(not_null<QWidget*> parent) {
const auto thread = _sectionsSlice[index];
if (ranges::contains(_slice, thread)) {
scrollSavingThread = thread;
scrollSavingShift = scrollTop
- tabs->lookupSectionTop(index);
scrollSavingShift = scrollValue
- slider->lookupSectionPosition(index);
scrollSavingIndex = index;
break;
}
@ -675,20 +334,27 @@ void SubsectionTabs::setupVertical(not_null<QWidget*> parent) {
}
}
tabs->setSections(sections, paused);
tabs->fitHeightToSections();
tabs->setActiveSectionFast(activeIndex);
slider->setSections({
.tabs = std::move(sections),
.context = Core::TextContext({
.session = &_history->session(),
}),
}, paused);
slider->setActiveSectionFast(activeIndex);
_sectionsSlice = _slice;
_vertical->resize(
std::max(toggle->width(), tabs->width()),
_vertical->height());
if (scrollSavingIndex >= 0) {
scroll->scrollToY(tabs->lookupSectionTop(scrollSavingIndex)
+ scrollSavingShift);
const auto position = scrollSavingShift
+ slider->lookupSectionPosition(scrollSavingIndex);
if (vertical) {
scroll->scrollToY(position);
} else {
scroll->scrollToX(position);
}
}
_scrollCheckRequests.fire({});
}, _vertical->lifetime());
}, scroll->lifetime());
}
void SubsectionTabs::loadMore() {
@ -910,9 +576,7 @@ bool SubsectionTabs::UsedFor(not_null<Data::Thread*> thread) {
return true;
}
const auto channel = history->peer->asChannel();
return channel
&& channel->isForum()
&& ((channel->flags() & ChannelDataFlag::ForumTabs) || true); AssertIsDebug();
return channel && channel->useSubsectionTabs();
}
} // namespace HistoryView

View file

@ -19,6 +19,8 @@ class SessionController;
namespace Ui {
class RpWidget;
class ScrollArea;
class SubsectionSlider;
} // namespace Ui
namespace HistoryView {
@ -60,6 +62,11 @@ private:
void loadMore();
[[nodiscard]] rpl::producer<> dataChanged() const;
void setupSlider(
not_null<Ui::ScrollArea*> scroll,
not_null<Ui::SubsectionSlider*> slider,
bool vertical);
const not_null<Window::SessionController*> _controller;
const not_null<History*> _history;

View file

@ -1255,7 +1255,7 @@ newPeerWidth: 320px;
swipeBackSize: 150px;
chatTabsToggle: IconButton(defaultIconButton) {
width: 56px;
width: 64px;
height: 36px;
icon: icon {{ "top_bar_profile-flip_horizontal", menuIconFg }};
iconOver: icon {{ "top_bar_profile-flip_horizontal", menuIconFgOver }};
@ -1285,6 +1285,13 @@ chatTabsSlider: SettingsSlider(defaultSettingsSlider) {
ripple: defaultRippleAnimation;
}
ChatTabsOutline {
radius: pixels;
stroke: pixels;
fg: color;
skip: pixels;
}
ChatTabsVertical {
barStroke: pixels;
barRadius: pixels;
@ -1311,16 +1318,26 @@ chatTabsVertical: ChatTabsVertical {
nameStyle: TextStyle(defaultTextStyle) {
font: font(10px);
}
nameWidth: 46px;
nameTop: 46px;
nameWidth: 54px;
nameTop: 42px;
nameFg: windowSubTextFg;
nameFgActive: lightButtonFg;
userpicTop: 8px;
userpicSize: 36px;
baseHeight: 56px;
width: 56px;
userpicSize: 28px;
baseHeight: 50px;
width: 64px;
ripple: defaultRippleAnimation;
rippleBg: windowBgOver;
rippleBgActive: lightButtonBgOver;
duration: 150;
}
chatTabsOutlineHorizontal: ChatTabsOutline {
stroke: 8px;
radius: 4px;
fg: sliderBgActive;
skip: 8px;
}
chatTabsOutlineVertical: ChatTabsOutline(chatTabsOutlineHorizontal) {
}

View file

@ -0,0 +1,487 @@
/*
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/controls/subsection_tabs_slider.h"
#include "base/call_delayed.h"
#include "ui/effects/ripple_animation.h"
#include "ui/dynamic_image.h"
#include "styles/style_chat.h"
#include "styles/style_filter_icons.h"
namespace Ui {
namespace {
constexpr auto kMaxNameLines = 3;
class VerticalButton final : public SubsectionButton {
public:
VerticalButton(
not_null<QWidget*> parent,
not_null<SubsectionButtonDelegate*> delegate,
SubsectionTab &&data);
private:
void paintEvent(QPaintEvent *e) override;
void dataUpdatedHook() override;
void updateSize();
const style::ChatTabsVertical &_st;
Text::String _text;
bool _subscribed = false;
};
class HorizontalButton final : public SubsectionButton {
public:
HorizontalButton(
not_null<QWidget*> parent,
const style::SettingsSlider &st,
not_null<SubsectionButtonDelegate*> delegate,
SubsectionTab &&data);
private:
void paintEvent(QPaintEvent *e) override;
void dataUpdatedHook() override;
void updateSize();
const style::SettingsSlider &_st;
Text::String _text;
};
VerticalButton::VerticalButton(
not_null<QWidget*> parent,
not_null<SubsectionButtonDelegate*> delegate,
SubsectionTab &&data)
: SubsectionButton(parent, delegate, std::move(data))
, _st(st::chatTabsVertical)
, _text(_st.nameStyle, _data.text, kDefaultTextOptions, _st.nameWidth) {
updateSize();
}
void VerticalButton::dataUpdatedHook() {
_text.setMarkedText(_st.nameStyle, _data.text, kDefaultTextOptions);
updateSize();
}
void VerticalButton::updateSize() {
resize(_st.width, _st.baseHeight + std::min(
_st.nameStyle.font->height * kMaxNameLines,
_text.countHeight(_st.nameWidth, true)));
}
void VerticalButton::paintEvent(QPaintEvent *e) {
auto p = QPainter(this);
const auto active = _delegate->buttonActive(this);
const auto color = anim::color(
_st.rippleBg,
_st.rippleBgActive,
active);
paintRipple(p, QPoint(0, 0), &color);
if (!_subscribed) {
_subscribed = true;
_data.userpic->subscribeToUpdates([=] { update(); });
}
const auto &image = _data.userpic->image(_st.userpicSize);
const auto userpicLeft = (width() - _st.userpicSize) / 2;
p.drawImage(userpicLeft, _st.userpicTop, image);
p.setPen(anim::pen(_st.nameFg, _st.nameFgActive, active));
const auto textLeft = (width() - _st.nameWidth) / 2;
_text.draw(p, {
.position = QPoint(textLeft, _st.nameTop),
.outerWidth = width(),
.availableWidth = _st.nameWidth,
.align = style::al_top,
.paused = _delegate->buttonPaused(),
});
}
HorizontalButton::HorizontalButton(
not_null<QWidget*> parent,
const style::SettingsSlider &st,
not_null<SubsectionButtonDelegate*> delegate,
SubsectionTab &&data)
: SubsectionButton(parent, delegate, std::move(data))
, _st(st) {
dataUpdatedHook();
}
void HorizontalButton::updateSize() {
resize(_st.strictSkip + _text.maxWidth(), _st.height);
}
void HorizontalButton::dataUpdatedHook() {
auto context = _delegate->buttonContext();
context.repaint = [=] { update(); };
_text.setMarkedText(
_st.labelStyle,
_data.text,
kDefaultTextOptions,
context);
updateSize();
}
void HorizontalButton::paintEvent(QPaintEvent *e) {
auto p = QPainter(this);
const auto active = _delegate->buttonActive(this);
const auto color = anim::color(
_st.rippleBg,
_st.rippleBgActive,
active);
paintRipple(p, QPoint(0, 0), &color);
p.setPen(anim::pen(_st.labelFg, _st.labelFgActive, active));
_text.draw(p, {
.position = QPoint(_st.strictSkip / 2, _st.labelTop),
.outerWidth = width(),
.availableWidth = _text.maxWidth(),
.paused = _delegate->buttonPaused(),
});
}
} // namespace
SubsectionButton::SubsectionButton(
not_null<QWidget*> parent,
not_null<SubsectionButtonDelegate*> delegate,
SubsectionTab &&data)
: RippleButton(parent, st::defaultRippleAnimationBgOver)
, _delegate(delegate)
, _data(std::move(data)) {
}
SubsectionButton::~SubsectionButton() = default;
void SubsectionButton::setData(SubsectionTab &&data) {
_data = std::move(data);
dataUpdatedHook();
update();
}
DynamicImage *SubsectionButton::userpic() const {
return _data.userpic.get();
}
void SubsectionButton::setActiveShown(float64 activeShown) {
if (_activeShown != activeShown) {
_activeShown = activeShown;
update();
}
}
SubsectionSlider::SubsectionSlider(not_null<QWidget*> parent, bool vertical)
: RpWidget(parent)
, _vertical(vertical)
, _barSt(vertical
? st::chatTabsOutlineVertical
: st::chatTabsOutlineHorizontal)
, _bar(CreateChild<RpWidget>(this))
, _barRect(_barSt.radius, _barSt.fg) {
setupBar();
}
SubsectionSlider::~SubsectionSlider() = default;
void SubsectionSlider::setupBar() {
_bar->setAttribute(Qt::WA_TransparentForMouseEvents);
sizeValue() | rpl::start_with_next([=](QSize size) {
const auto thickness = _barSt.stroke - (_barSt.stroke / 2);
_bar->setGeometry(
0,
_vertical ? 0 : (size.height() - thickness),
_vertical ? thickness : size.width(),
_vertical ? size.height() : thickness);
}, _bar->lifetime());
_bar->paintRequest() | rpl::start_with_next([=](QRect clip) {
const auto start = -_barSt.stroke / 2;
const auto finalRange = getFinalActiveRange();
const auto currentRange = getCurrentActiveRange();
const auto from = currentRange.from + _barSt.skip;
const auto size = currentRange.size - 2 * _barSt.skip;
if (size <= 0) {
return;
}
const auto rect = myrtlrect(
_vertical ? start : from,
_vertical ? from : 0,
_vertical ? _barSt.stroke : size,
_vertical ? size : _barSt.stroke);
if (rect.intersects(clip)) {
auto p = QPainter(_bar);
_barRect.paint(p, rect);
}
}, _bar->lifetime());
}
void SubsectionSlider::setSections(
SubsectionTabs sections,
Fn<bool()> paused) {
Expects(!sections.tabs.empty());
_context = sections.context;
_paused = std::move(paused);
_fixedCount = sections.fixed;
_pinnedCount = sections.pinned;
_reorderAllowed = sections.reorder;
auto old = base::take(_tabs);
_tabs.reserve(sections.tabs.size());
auto size = 0;
for (auto &data : sections.tabs) {
const auto i = data.userpic
? ranges::find(
old,
data.userpic.get(),
&SubsectionButton::userpic)
: old.empty()
? end(old)
: (end(old) - 1);
if (i != end(old)) {
_tabs.push_back(std::move(*i));
old.erase(i);
_tabs.back()->setData(std::move(data));
} else {
_tabs.push_back(makeButton(std::move(data)));
_tabs.back()->show();
}
_tabs.back()->move(_vertical ? 0 : size, _vertical ? size : 0);
const auto index = int(_tabs.size()) - 1;
_tabs.back()->setClickedCallback([=] {
activate(index);
});
size += _vertical ? _tabs.back()->height() : _tabs.back()->width();
}
if (!_tabs.empty()) {
resize(
_vertical ? _tabs.front()->width() : size,
_vertical ? size : _tabs.front()->height());
}
_bar->raise();
}
void SubsectionSlider::activate(int index) {
if (_active == index) {
return;
}
const auto old = _active;
const auto was = getFinalActiveRange();
_active = index;
const auto now = getFinalActiveRange();
const auto callback = [=] {
_bar->update();
for (auto i = std::min(old, index); i != std::max(old, index); ++i) {
if (i >= 0 && i < int(_tabs.size())) {
_tabs[i]->update();
}
}
};
const auto duration = st::chatTabsSlider.duration;
_activeFrom.start(callback, was.from, now.from, duration);
_activeSize.start(callback, was.size, now.size, duration);
base::call_delayed(duration, this, [=] {
if (_active == index) {
_sectionActivated.fire_copy(index);
}
});
}
void SubsectionSlider::setActiveSectionFast(int active) {
Expects(active < int(_tabs.size()));
_active = active;
_activeFrom.stop();
_activeSize.stop();
_bar->update();
}
int SubsectionSlider::sectionsCount() const {
return int(_tabs.size());
}
rpl::producer<int> SubsectionSlider::sectionActivated() const {
return _sectionActivated.events();
}
int SubsectionSlider::lookupSectionPosition(int index) const {
Expects(index >= 0 && index < _tabs.size());
return _vertical ? _tabs[index]->y() : _tabs[index]->x();
}
void SubsectionSlider::paintEvent(QPaintEvent *e) {
}
int SubsectionSlider::lookupSectionIndex(QPoint position) const {
Expects(!_tabs.empty());
const auto count = sectionsCount();
if (_vertical) {
for (auto i = 0; i != count; ++i) {
const auto tab = _tabs[i].get();
if (position.y() < tab->y() + tab->height()) {
return i;
}
}
} else {
for (auto i = 0; i != count; ++i) {
const auto tab = _tabs[i].get();
if (position.x() < tab->x() + tab->width()) {
return i;
}
}
}
return count - 1;
}
SubsectionSlider::Range SubsectionSlider::getFinalActiveRange() const {
if (_active < 0 || _active >= _tabs.size()) {
return {};
}
const auto tab = _tabs[_active].get();
return Range{
.from = _vertical ? tab->y() : tab->x(),
.size = _vertical ? tab->height() : tab->width(),
};
}
SubsectionSlider::Range SubsectionSlider::getCurrentActiveRange() const {
const auto finalRange = getFinalActiveRange();
return {
.from = int(base::SafeRound(_activeFrom.value(finalRange.from))),
.size = int(base::SafeRound(_activeSize.value(finalRange.size))),
};
}
bool SubsectionSlider::buttonPaused() {
return _paused && _paused();
}
float64 SubsectionSlider::buttonActive(not_null<SubsectionButton*> button) {
const auto finalRange = getFinalActiveRange();
const auto currentRange = getCurrentActiveRange();
const auto from = _vertical ? button->y() : button->x();
const auto size = _vertical ? button->height() : button->width();
const auto checkSize = std::min(size, currentRange.size);
return (checkSize > 0)
? (1. - (std::abs(currentRange.from - from) / float64(checkSize)))
: 0.;
}
Text::MarkedContext SubsectionSlider::buttonContext() {
return _context;
}
not_null<SubsectionButton*> SubsectionSlider::buttonAt(int index) {
Expects(index >= 0 && index < _tabs.size());
return _tabs[index].get();
}
VerticalSlider::VerticalSlider(not_null<QWidget*> parent)
: SubsectionSlider(parent, true)
, _st(st::chatTabsVertical) {
}
VerticalSlider::~VerticalSlider() = default;
std::unique_ptr<SubsectionButton> VerticalSlider::makeButton(
SubsectionTab &&data) {
return std::make_unique<VerticalButton>(
this,
static_cast<SubsectionButtonDelegate*>(this),
std::move(data));
}
HorizontalSlider::HorizontalSlider(not_null<QWidget*> parent)
: SubsectionSlider(parent, false)
, _st(st::chatTabsSlider) {
}
HorizontalSlider::~HorizontalSlider() = default;
std::unique_ptr<SubsectionButton> HorizontalSlider::makeButton(
SubsectionTab &&data) {
return std::make_unique<HorizontalButton>(
this,
_st,
static_cast<SubsectionButtonDelegate*>(this),
std::move(data));
}
std::shared_ptr<DynamicImage> MakeAllSubsectionsThumbnail(
Fn<QColor()> textColor) {
class Image final : public DynamicImage {
public:
Image(Fn<QColor()> textColor) : _textColor(std::move(textColor)) {
Expects(_textColor != nullptr);
}
std::shared_ptr<DynamicImage> clone() {
return std::make_shared<Image>(_textColor);
}
QImage image(int size) {
const auto ratio = style::DevicePixelRatio();
const auto full = size * ratio;
const auto color = _textColor();
if (_cache.size() != QSize(full, full)) {
_cache = QImage(
QSize(full, full),
QImage::Format_ARGB32_Premultiplied);
_cache.fill(Qt::TransparentMode);
} else if (_color == color) {
return _cache;
}
_color = color;
if (_mask.isNull()) {
_mask = st::foldersAll.instance(QColor(255, 255, 255));
}
const auto position = ratio * QPoint(
(size - (_mask.width() / ratio)) / 2,
(size - (_mask.height() / ratio)) / 2);
if (_mask.width() <= full && _mask.height() <= full) {
style::colorizeImage(_mask, color, &_cache, QRect(), position);
} else {
_cache = style::colorizeImage(_mask, color).scaled(
full,
full,
Qt::IgnoreAspectRatio,
Qt::SmoothTransformation);
_cache.setDevicePixelRatio(ratio);
}
return _cache;
}
void subscribeToUpdates(Fn<void()> callback) {
if (!callback) {
_cache = QImage();
_mask = QImage();
}
}
private:
Fn<QColor()> _textColor;
QImage _mask;
QImage _cache;
QColor _color;
};
return std::make_shared<Image>(std::move(textColor));
}
} // namespace Ui

View file

@ -0,0 +1,163 @@
/*
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/round_rect.h"
#include "ui/rp_widget.h"
#include "ui/widgets/buttons.h"
namespace style {
struct ChatTabsVertical;
struct ChatTabsOutline;
} // namespace style
namespace Ui {
class DynamicImage;
class RippleAnimation;
class SubsectionButton;
struct SubsectionTab {
TextWithEntities text;
std::shared_ptr<DynamicImage> userpic;
int counter = 0;
bool muted = false;
bool mention = false;
bool reaciton = false;
};
struct SubsectionTabs {
std::vector<SubsectionTab> tabs;
Text::MarkedContext context;
int fixed = 0;
int pinned = 0;
bool reorder = false;
};
class SubsectionButtonDelegate {
public:
virtual bool buttonPaused() = 0;
virtual float64 buttonActive(not_null<SubsectionButton*> button) = 0;
virtual Text::MarkedContext buttonContext() = 0;
};
class SubsectionButton : public RippleButton {
public:
SubsectionButton(
not_null<QWidget*> parent,
not_null<SubsectionButtonDelegate*> delegate,
SubsectionTab &&data);
~SubsectionButton();
void setData(SubsectionTab &&data);
[[nodiscard]] DynamicImage *userpic() const;
void setActiveShown(float64 activeShown);
protected:
virtual void dataUpdatedHook() = 0;
const not_null<SubsectionButtonDelegate*> _delegate;
SubsectionTab _data;
float64 _activeShown = 0.;
};
class SubsectionSlider
: public RpWidget
, public SubsectionButtonDelegate {
public:
~SubsectionSlider();
void setSections(
SubsectionTabs sections,
Fn<bool()> paused);
void setActiveSectionFast(int active);
[[nodiscard]] int sectionsCount() const;
[[nodiscard]] rpl::producer<int> sectionActivated() const;
[[nodiscard]] int lookupSectionPosition(int index) const;
bool buttonPaused() override;
float64 buttonActive(not_null<SubsectionButton*> button) override;
Text::MarkedContext buttonContext() override;
[[nodiscard]] not_null<SubsectionButton*> buttonAt(int index);
protected:
struct Range {
int from = 0;
int size = 0;
};
SubsectionSlider(not_null<QWidget*> parent, bool vertical);
void setupBar();
void paintEvent(QPaintEvent *e) override;
[[nodiscard]] int lookupSectionIndex(QPoint position) const;
[[nodiscard]] Range getFinalActiveRange() const;
[[nodiscard]] Range getCurrentActiveRange() const;
void activate(int index);
[[nodiscard]] virtual std::unique_ptr<SubsectionButton> makeButton(
SubsectionTab &&data) = 0;
const bool _vertical = false;
const style::ChatTabsOutline &_barSt;
RpWidget *_bar = nullptr;
RoundRect _barRect;
std::vector<std::unique_ptr<SubsectionButton>> _tabs;
int _active = -1;
int _pressed = -1;
Animations::Simple _activeFrom;
Animations::Simple _activeSize;
//int _buttonIndexHint = 0;
Text::MarkedContext _context;
int _fixedCount = 0;
int _pinnedCount = 0;
bool _reorderAllowed = false;
rpl::event_stream<int> _sectionActivated;
Fn<bool()> _paused;
};
class VerticalSlider final : public SubsectionSlider {
public:
explicit VerticalSlider(not_null<QWidget*> parent);
~VerticalSlider();
private:
std::unique_ptr<SubsectionButton> makeButton(
SubsectionTab &&data) override;
const style::ChatTabsVertical &_st;
};
class HorizontalSlider final : public SubsectionSlider {
public:
explicit HorizontalSlider(not_null<QWidget*> parent);
~HorizontalSlider();
private:
std::unique_ptr<SubsectionButton> makeButton(
SubsectionTab &&data) override;
const style::SettingsSlider &_st;
};
[[nodiscard]] std::shared_ptr<DynamicImage> MakeAllSubsectionsThumbnail(
Fn<QColor()> textColor);
} // namespace Ui

View file

@ -196,7 +196,11 @@ private:
class EmojiThumbnail final : public DynamicImage {
public:
EmojiThumbnail(not_null<Data::Session*> owner, const QString &data);
EmojiThumbnail(
not_null<Data::Session*> owner,
const QString &data,
Fn<bool()> paused,
Fn<QColor()> textColor);
std::shared_ptr<DynamicImage> clone() override;
@ -207,6 +211,8 @@ private:
const not_null<Data::Session*> _owner;
const QString _data;
std::unique_ptr<Ui::Text::CustomEmoji> _emoji;
Fn<bool()> _paused;
Fn<QColor()> _textColor;
QImage _frame;
};
@ -581,9 +587,13 @@ void IconThumbnail::subscribeToUpdates(Fn<void()> callback) {
EmojiThumbnail::EmojiThumbnail(
not_null<Data::Session*> owner,
const QString &data)
const QString &data,
Fn<bool()> paused,
Fn<QColor()> textColor)
: _owner(owner)
, _data(data) {
, _data(data)
, _paused(std::move(paused))
, _textColor(std::move(textColor)) {
}
void EmojiThumbnail::subscribeToUpdates(Fn<void()> callback) {
@ -598,7 +608,11 @@ void EmojiThumbnail::subscribeToUpdates(Fn<void()> callback) {
}
std::shared_ptr<DynamicImage> EmojiThumbnail::clone() {
return std::make_shared<EmojiThumbnail>(_owner, _data);
return std::make_shared<EmojiThumbnail>(
_owner,
_data,
_paused,
_textColor);
}
QImage EmojiThumbnail::image(int size) {
@ -614,12 +628,16 @@ QImage EmojiThumbnail::image(int size) {
}
_frame.fill(Qt::transparent);
const auto esize = Text::AdjustCustomEmojiSize(
Emoji::GetSizeLarge() / style::DevicePixelRatio());
const auto eskip = (size - esize) / 2;
auto p = Painter(&_frame);
_emoji->paint(p, {
.textColor = st::windowBoldFg->c,
.textColor = _textColor ? _textColor() : st::windowBoldFg->c,
.now = crl::now(),
.position = QPoint(0, 0),
.paused = false,
.position = QPoint(eskip, eskip),
.paused = _paused && _paused(),
});
p.end();
@ -665,8 +683,14 @@ std::shared_ptr<DynamicImage> MakeIconThumbnail(const style::icon &icon) {
std::shared_ptr<DynamicImage> MakeEmojiThumbnail(
not_null<Data::Session*> owner,
const QString &data) {
return std::make_shared<EmojiThumbnail>(owner, data);
const QString &data,
Fn<bool()> paused,
Fn<QColor()> textColor) {
return std::make_shared<EmojiThumbnail>(
owner,
data,
std::move(paused),
std::move(textColor));
}
std::shared_ptr<DynamicImage> MakePhotoThumbnail(

View file

@ -33,7 +33,9 @@ class DynamicImage;
const style::icon &icon);
[[nodiscard]] std::shared_ptr<DynamicImage> MakeEmojiThumbnail(
not_null<Data::Session*> owner,
const QString &data);
const QString &data,
Fn<bool()> paused = false,
Fn<QColor()> textColor = nullptr);
[[nodiscard]] std::shared_ptr<DynamicImage> MakePhotoThumbnail(
not_null<PhotoData*> photo,
FullMsgId fullId);

View file

@ -386,6 +386,8 @@ PRIVATE
ui/controls/send_as_button.h
ui/controls/send_button.cpp
ui/controls/send_button.h
ui/controls/subsection_tabs_slider.cpp
ui/controls/subsection_tabs_slider.h
ui/controls/swipe_handler.cpp
ui/controls/swipe_handler.h
ui/controls/swipe_handler_data.h