mirror of
https://github.com/AyuGram/AyuGramDesktop.git
synced 2025-07-27 07:52:57 +02:00
Implement better horizontal/vertical tabs.
This commit is contained in:
parent
0e5419c60b
commit
8512154b45
14 changed files with 908 additions and 523 deletions
|
@ -93,11 +93,12 @@ void DefaultIconEmoji::paint(QPainter &p, const Context &context) {
|
||||||
const auto &st = (_tag == Data::CustomEmojiSizeTag::Normal)
|
const auto &st = (_tag == Data::CustomEmojiSizeTag::Normal)
|
||||||
? st::normalForumTopicIcon
|
? st::normalForumTopicIcon
|
||||||
: st::defaultForumTopicIcon;
|
: st::defaultForumTopicIcon;
|
||||||
|
const auto general = Data::IsForumGeneralIconTitle(_icon.title);
|
||||||
if (_image.isNull()) {
|
if (_image.isNull()) {
|
||||||
_image = Data::IsForumGeneralIconTitle(_icon.title)
|
_image = general
|
||||||
? Data::ForumTopicGeneralIconFrame(
|
? Data::ForumTopicGeneralIconFrame(
|
||||||
st.size,
|
st.size,
|
||||||
Data::ParseForumGeneralIconColor(_icon.colorId))
|
QColor(255, 255, 255))
|
||||||
: Data::ForumTopicIconFrame(_icon.colorId, _icon.title, st);
|
: Data::ForumTopicIconFrame(_icon.colorId, _icon.title, st);
|
||||||
}
|
}
|
||||||
const auto full = (_tag == Data::CustomEmojiSizeTag::Normal)
|
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 esize = full / style::DevicePixelRatio();
|
||||||
const auto customSize = Ui::Text::AdjustCustomEmojiSize(esize);
|
const auto customSize = Ui::Text::AdjustCustomEmojiSize(esize);
|
||||||
const auto skip = (customSize - st.size) / 2;
|
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() {
|
void DefaultIconEmoji::unload() {
|
||||||
|
|
|
@ -408,6 +408,11 @@ void ChannelData::setPendingRequestsCount(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool ChannelData::useSubsectionTabs() const {
|
||||||
|
return isForum()
|
||||||
|
&& ((flags() & ChannelDataFlag::ForumTabs) || true); AssertIsDebug();
|
||||||
|
}
|
||||||
|
|
||||||
ChatRestrictionsInfo ChannelData::KickedRestrictedRights(
|
ChatRestrictionsInfo ChannelData::KickedRestrictedRights(
|
||||||
not_null<PeerData*> participant) {
|
not_null<PeerData*> participant) {
|
||||||
using Flag = ChatRestriction;
|
using Flag = ChatRestriction;
|
||||||
|
|
|
@ -279,6 +279,7 @@ public:
|
||||||
[[nodiscard]] bool paidMessagesAvailable() const {
|
[[nodiscard]] bool paidMessagesAvailable() const {
|
||||||
return flags() & Flag::PaidMessagesAvailable;
|
return flags() & Flag::PaidMessagesAvailable;
|
||||||
}
|
}
|
||||||
|
[[nodiscard]] bool useSubsectionTabs() const;
|
||||||
|
|
||||||
[[nodiscard]] static ChatRestrictionsInfo KickedRestrictedRights(
|
[[nodiscard]] static ChatRestrictionsInfo KickedRestrictedRights(
|
||||||
not_null<PeerData*> participant);
|
not_null<PeerData*> participant);
|
||||||
|
|
|
@ -152,10 +152,10 @@ QImage ForumTopicGeneralIconFrame(int size, const QColor &color) {
|
||||||
result.setDevicePixelRatio(ratio);
|
result.setDevicePixelRatio(ratio);
|
||||||
result.fill(Qt::transparent);
|
result.fill(Qt::transparent);
|
||||||
|
|
||||||
const auto use = size * 0.8;
|
const auto use = size * 1.;
|
||||||
const auto skip = size * 0.1;
|
const auto skip = size * 0.;
|
||||||
auto p = QPainter(&result);
|
auto p = QPainter(&result);
|
||||||
svg.render(&p, QRectF(skip, 0, use, use));
|
svg.render(&p, QRectF(skip, skip, use, use));
|
||||||
p.end();
|
p.end();
|
||||||
|
|
||||||
return style::colorizeImage(result, color);
|
return style::colorizeImage(result, color);
|
||||||
|
|
|
@ -500,8 +500,8 @@ dialogsLoadMoreLoading: InfiniteRadialAnimation(defaultInfiniteRadialAnimation)
|
||||||
}
|
}
|
||||||
|
|
||||||
dialogsSearchInHeight: 38px;
|
dialogsSearchInHeight: 38px;
|
||||||
dialogsSearchInPhotoSize: 26px;
|
dialogsSearchInPhotoSize: 28px;
|
||||||
dialogsSearchInPhotoPadding: 12px;
|
dialogsSearchInPhotoPadding: 10px;
|
||||||
dialogsSearchInSkip: 10px;
|
dialogsSearchInSkip: 10px;
|
||||||
dialogsSearchInNameTop: 9px;
|
dialogsSearchInNameTop: 9px;
|
||||||
dialogsSearchInDownTop: 15px;
|
dialogsSearchInDownTop: 15px;
|
||||||
|
|
|
@ -4218,12 +4218,20 @@ void InnerWidget::updateSearchIn() {
|
||||||
: _openedForum
|
: _openedForum
|
||||||
? _openedForum->channel().get()
|
? _openedForum->channel().get()
|
||||||
: nullptr;
|
: nullptr;
|
||||||
|
const auto paused = [window = _controller] {
|
||||||
|
return window->isGifPausedAtLeastFor(Window::GifPauseReason::Any);
|
||||||
|
};
|
||||||
|
const auto textFg = [] {
|
||||||
|
return st::windowSubTextFg->c;
|
||||||
|
};
|
||||||
const auto topicIcon = !topic
|
const auto topicIcon = !topic
|
||||||
? nullptr
|
? nullptr
|
||||||
: topic->iconId()
|
: topic->iconId()
|
||||||
? Ui::MakeEmojiThumbnail(
|
? Ui::MakeEmojiThumbnail(
|
||||||
&topic->owner(),
|
&topic->owner(),
|
||||||
Data::SerializeCustomEmojiId(topic->iconId()))
|
Data::SerializeCustomEmojiId(topic->iconId()),
|
||||||
|
paused,
|
||||||
|
textFg)
|
||||||
: Ui::MakeEmojiThumbnail(
|
: Ui::MakeEmojiThumbnail(
|
||||||
&topic->owner(),
|
&topic->owner(),
|
||||||
Data::TopicIconEmojiEntity({
|
Data::TopicIconEmojiEntity({
|
||||||
|
@ -4233,7 +4241,9 @@ void InnerWidget::updateSearchIn() {
|
||||||
.colorId = (topic->isGeneral()
|
.colorId = (topic->isGeneral()
|
||||||
? Data::ForumGeneralIconColor(st::windowSubTextFg->c)
|
? Data::ForumGeneralIconColor(st::windowSubTextFg->c)
|
||||||
: topic->colorId()),
|
: topic->colorId()),
|
||||||
}));
|
}),
|
||||||
|
paused,
|
||||||
|
textFg);
|
||||||
const auto peerIcon = peer
|
const auto peerIcon = peer
|
||||||
? Ui::MakeUserpicThumbnail(peer)
|
? Ui::MakeUserpicThumbnail(peer)
|
||||||
: sublist
|
: sublist
|
||||||
|
|
|
@ -21,6 +21,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||||
#include "history/history.h"
|
#include "history/history.h"
|
||||||
#include "lang/lang_keys.h"
|
#include "lang/lang_keys.h"
|
||||||
#include "main/main_session.h"
|
#include "main/main_session.h"
|
||||||
|
#include "ui/controls/subsection_tabs_slider.h"
|
||||||
#include "ui/effects/ripple_animation.h"
|
#include "ui/effects/ripple_animation.h"
|
||||||
#include "ui/text/text_utilities.h"
|
#include "ui/text/text_utilities.h"
|
||||||
#include "ui/widgets/buttons.h"
|
#include "ui/widgets/buttons.h"
|
||||||
|
@ -36,284 +37,6 @@ namespace HistoryView {
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
constexpr auto kDefaultLimit = 5; AssertIsDebug()// 10;
|
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 §ion : 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
|
} // namespace
|
||||||
|
|
||||||
|
@ -370,18 +93,13 @@ void SubsectionTabs::setupHorizontal(not_null<QWidget*> parent) {
|
||||||
st::chatTabsScroll,
|
st::chatTabsScroll,
|
||||||
true);
|
true);
|
||||||
scroll->show();
|
scroll->show();
|
||||||
const auto tabs = scroll->setOwnedWidget(
|
const auto slider = scroll->setOwnedWidget(
|
||||||
object_ptr<Ui::SettingsSlider>(scroll, st::chatTabsSlider));
|
object_ptr<Ui::HorizontalSlider>(scroll));
|
||||||
tabs->sectionActivated() | rpl::start_with_next([=](int active) {
|
setupSlider(scroll, slider, false);
|
||||||
if (active >= 0
|
|
||||||
&& active < _slice.size()
|
_horizontal->resize(
|
||||||
&& _active != _slice[active]) {
|
_horizontal->width(),
|
||||||
auto params = Window::SectionShow();
|
std::max(toggle->height(), slider->height()));
|
||||||
params.way = Window::SectionShow::Way::ClearStack;
|
|
||||||
params.animated = anim::type::instant;
|
|
||||||
_controller->showThread(_slice[active], {}, params);
|
|
||||||
}
|
|
||||||
}, tabs->lifetime());
|
|
||||||
|
|
||||||
scroll->setCustomWheelProcess([=](not_null<QWheelEvent*> e) {
|
scroll->setCustomWheelProcess([=](not_null<QWheelEvent*> e) {
|
||||||
const auto pixelDelta = e->pixelDelta();
|
const auto pixelDelta = e->pixelDelta();
|
||||||
|
@ -394,36 +112,6 @@ void SubsectionTabs::setupHorizontal(not_null<QWidget*> parent) {
|
||||||
return true;
|
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(
|
_horizontal->sizeValue(
|
||||||
) | rpl::start_with_next([=](QSize size) {
|
) | rpl::start_with_next([=](QSize size) {
|
||||||
const auto togglew = toggle->width();
|
const auto togglew = toggle->width();
|
||||||
|
@ -438,89 +126,6 @@ void SubsectionTabs::setupHorizontal(not_null<QWidget*> parent) {
|
||||||
{ 0, 0, 0, st::lineWidth })),
|
{ 0, 0, 0, st::lineWidth })),
|
||||||
st::windowBg);
|
st::windowBg);
|
||||||
}, _horizontal->lifetime());
|
}, _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) {
|
void SubsectionTabs::setupVertical(not_null<QWidget*> parent) {
|
||||||
|
@ -547,48 +152,14 @@ void SubsectionTabs::setupVertical(not_null<QWidget*> parent) {
|
||||||
_vertical,
|
_vertical,
|
||||||
st::chatTabsScroll);
|
st::chatTabsScroll);
|
||||||
scroll->show();
|
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(
|
const auto slider = scroll->setOwnedWidget(
|
||||||
scroll->scrolls(),
|
object_ptr<Ui::VerticalSlider>(scroll));
|
||||||
_scrollCheckRequests.events(),
|
setupSlider(scroll, slider, true);
|
||||||
scroll->heightValue() | rpl::skip(1) | rpl::map_to(rpl::empty)
|
|
||||||
) | rpl::start_with_next([=] {
|
_vertical->resize(
|
||||||
const auto height = scroll->height();
|
std::max(toggle->width(), slider->width()),
|
||||||
const auto top = scroll->scrollTop();
|
_vertical->height());
|
||||||
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());
|
|
||||||
|
|
||||||
_vertical->sizeValue(
|
_vertical->sizeValue(
|
||||||
) | rpl::start_with_next([=](QSize size) {
|
) | 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) {
|
_vertical->paintRequest() | rpl::start_with_next([=](QRect clip) {
|
||||||
QPainter(_vertical).fillRect(clip, st::windowBg);
|
QPainter(_vertical).fillRect(clip, st::windowBg);
|
||||||
}, _vertical->lifetime());
|
}, _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(
|
_refreshed.events_starting_with_copy(
|
||||||
rpl::empty
|
rpl::empty
|
||||||
) | rpl::start_with_next([=] {
|
) | rpl::start_with_next([=] {
|
||||||
auto sections = std::vector<VerticalSlider::Section>();
|
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({
|
|
||||||
.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 paused = [=] {
|
const auto paused = [=] {
|
||||||
return _controller->isGifPausedAtLeastFor(
|
return _controller->isGifPausedAtLeastFor(
|
||||||
Window::GifPauseReason::Any);
|
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 scrollSavingThread = (Data::Thread*)nullptr;
|
||||||
auto scrollSavingShift = 0;
|
auto scrollSavingShift = 0;
|
||||||
auto scrollSavingIndex = -1;
|
auto scrollSavingIndex = -1;
|
||||||
if (const auto count = tabs->sectionsCount()) {
|
if (const auto count = slider->sectionsCount()) {
|
||||||
const auto scrollTop = scroll->scrollTop();
|
const auto scrollValue = vertical
|
||||||
auto indexTop = tabs->lookupSectionTop(0);
|
? scroll->scrollTop()
|
||||||
|
: scroll->scrollLeft();
|
||||||
|
auto indexPosition = slider->lookupSectionPosition(0);
|
||||||
for (auto index = 0; index != count; ++index) {
|
for (auto index = 0; index != count; ++index) {
|
||||||
const auto nextTop = (index + 1 != count)
|
const auto nextPosition = (index + 1 != count)
|
||||||
? tabs->lookupSectionTop(index + 1)
|
? slider->lookupSectionPosition(index + 1)
|
||||||
: (indexTop + scrollTop + 1);
|
: (indexPosition + scrollValue + 1);
|
||||||
if (indexTop <= scrollTop && nextTop > scrollTop) {
|
if (indexPosition <= scrollValue && nextPosition > scrollValue) {
|
||||||
scrollSavingThread = _sectionsSlice[index];
|
scrollSavingThread = _sectionsSlice[index];
|
||||||
scrollSavingShift = scrollTop - indexTop;
|
scrollSavingShift = scrollValue - indexPosition;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
indexTop = nextTop;
|
indexPosition = nextPosition;
|
||||||
}
|
}
|
||||||
scrollSavingIndex = scrollSavingThread
|
scrollSavingIndex = scrollSavingThread
|
||||||
? int(ranges::find(_slice, not_null(scrollSavingThread))
|
? int(ranges::find(_slice, not_null(scrollSavingThread))
|
||||||
|
@ -666,8 +325,8 @@ void SubsectionTabs::setupVertical(not_null<QWidget*> parent) {
|
||||||
const auto thread = _sectionsSlice[index];
|
const auto thread = _sectionsSlice[index];
|
||||||
if (ranges::contains(_slice, thread)) {
|
if (ranges::contains(_slice, thread)) {
|
||||||
scrollSavingThread = thread;
|
scrollSavingThread = thread;
|
||||||
scrollSavingShift = scrollTop
|
scrollSavingShift = scrollValue
|
||||||
- tabs->lookupSectionTop(index);
|
- slider->lookupSectionPosition(index);
|
||||||
scrollSavingIndex = index;
|
scrollSavingIndex = index;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -675,20 +334,27 @@ void SubsectionTabs::setupVertical(not_null<QWidget*> parent) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
tabs->setSections(sections, paused);
|
slider->setSections({
|
||||||
tabs->fitHeightToSections();
|
.tabs = std::move(sections),
|
||||||
tabs->setActiveSectionFast(activeIndex);
|
.context = Core::TextContext({
|
||||||
|
.session = &_history->session(),
|
||||||
|
}),
|
||||||
|
}, paused);
|
||||||
|
slider->setActiveSectionFast(activeIndex);
|
||||||
|
|
||||||
_sectionsSlice = _slice;
|
_sectionsSlice = _slice;
|
||||||
_vertical->resize(
|
|
||||||
std::max(toggle->width(), tabs->width()),
|
|
||||||
_vertical->height());
|
|
||||||
if (scrollSavingIndex >= 0) {
|
if (scrollSavingIndex >= 0) {
|
||||||
scroll->scrollToY(tabs->lookupSectionTop(scrollSavingIndex)
|
const auto position = scrollSavingShift
|
||||||
+ scrollSavingShift);
|
+ slider->lookupSectionPosition(scrollSavingIndex);
|
||||||
|
if (vertical) {
|
||||||
|
scroll->scrollToY(position);
|
||||||
|
} else {
|
||||||
|
scroll->scrollToX(position);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_scrollCheckRequests.fire({});
|
_scrollCheckRequests.fire({});
|
||||||
}, _vertical->lifetime());
|
}, scroll->lifetime());
|
||||||
}
|
}
|
||||||
|
|
||||||
void SubsectionTabs::loadMore() {
|
void SubsectionTabs::loadMore() {
|
||||||
|
@ -910,9 +576,7 @@ bool SubsectionTabs::UsedFor(not_null<Data::Thread*> thread) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
const auto channel = history->peer->asChannel();
|
const auto channel = history->peer->asChannel();
|
||||||
return channel
|
return channel && channel->useSubsectionTabs();
|
||||||
&& channel->isForum()
|
|
||||||
&& ((channel->flags() & ChannelDataFlag::ForumTabs) || true); AssertIsDebug();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace HistoryView
|
} // namespace HistoryView
|
||||||
|
|
|
@ -19,6 +19,8 @@ class SessionController;
|
||||||
|
|
||||||
namespace Ui {
|
namespace Ui {
|
||||||
class RpWidget;
|
class RpWidget;
|
||||||
|
class ScrollArea;
|
||||||
|
class SubsectionSlider;
|
||||||
} // namespace Ui
|
} // namespace Ui
|
||||||
|
|
||||||
namespace HistoryView {
|
namespace HistoryView {
|
||||||
|
@ -60,6 +62,11 @@ private:
|
||||||
void loadMore();
|
void loadMore();
|
||||||
[[nodiscard]] rpl::producer<> dataChanged() const;
|
[[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<Window::SessionController*> _controller;
|
||||||
const not_null<History*> _history;
|
const not_null<History*> _history;
|
||||||
|
|
||||||
|
|
|
@ -1255,7 +1255,7 @@ newPeerWidth: 320px;
|
||||||
swipeBackSize: 150px;
|
swipeBackSize: 150px;
|
||||||
|
|
||||||
chatTabsToggle: IconButton(defaultIconButton) {
|
chatTabsToggle: IconButton(defaultIconButton) {
|
||||||
width: 56px;
|
width: 64px;
|
||||||
height: 36px;
|
height: 36px;
|
||||||
icon: icon {{ "top_bar_profile-flip_horizontal", menuIconFg }};
|
icon: icon {{ "top_bar_profile-flip_horizontal", menuIconFg }};
|
||||||
iconOver: icon {{ "top_bar_profile-flip_horizontal", menuIconFgOver }};
|
iconOver: icon {{ "top_bar_profile-flip_horizontal", menuIconFgOver }};
|
||||||
|
@ -1285,6 +1285,13 @@ chatTabsSlider: SettingsSlider(defaultSettingsSlider) {
|
||||||
ripple: defaultRippleAnimation;
|
ripple: defaultRippleAnimation;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ChatTabsOutline {
|
||||||
|
radius: pixels;
|
||||||
|
stroke: pixels;
|
||||||
|
fg: color;
|
||||||
|
skip: pixels;
|
||||||
|
}
|
||||||
|
|
||||||
ChatTabsVertical {
|
ChatTabsVertical {
|
||||||
barStroke: pixels;
|
barStroke: pixels;
|
||||||
barRadius: pixels;
|
barRadius: pixels;
|
||||||
|
@ -1311,16 +1318,26 @@ chatTabsVertical: ChatTabsVertical {
|
||||||
nameStyle: TextStyle(defaultTextStyle) {
|
nameStyle: TextStyle(defaultTextStyle) {
|
||||||
font: font(10px);
|
font: font(10px);
|
||||||
}
|
}
|
||||||
nameWidth: 46px;
|
nameWidth: 54px;
|
||||||
nameTop: 46px;
|
nameTop: 42px;
|
||||||
nameFg: windowSubTextFg;
|
nameFg: windowSubTextFg;
|
||||||
nameFgActive: lightButtonFg;
|
nameFgActive: lightButtonFg;
|
||||||
userpicTop: 8px;
|
userpicTop: 8px;
|
||||||
userpicSize: 36px;
|
userpicSize: 28px;
|
||||||
baseHeight: 56px;
|
baseHeight: 50px;
|
||||||
width: 56px;
|
width: 64px;
|
||||||
ripple: defaultRippleAnimation;
|
ripple: defaultRippleAnimation;
|
||||||
rippleBg: windowBgOver;
|
rippleBg: windowBgOver;
|
||||||
rippleBgActive: lightButtonBgOver;
|
rippleBgActive: lightButtonBgOver;
|
||||||
duration: 150;
|
duration: 150;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
chatTabsOutlineHorizontal: ChatTabsOutline {
|
||||||
|
stroke: 8px;
|
||||||
|
radius: 4px;
|
||||||
|
fg: sliderBgActive;
|
||||||
|
skip: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
chatTabsOutlineVertical: ChatTabsOutline(chatTabsOutlineHorizontal) {
|
||||||
|
}
|
||||||
|
|
487
Telegram/SourceFiles/ui/controls/subsection_tabs_slider.cpp
Normal file
487
Telegram/SourceFiles/ui/controls/subsection_tabs_slider.cpp
Normal 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
|
163
Telegram/SourceFiles/ui/controls/subsection_tabs_slider.h
Normal file
163
Telegram/SourceFiles/ui/controls/subsection_tabs_slider.h
Normal 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
|
|
@ -196,7 +196,11 @@ private:
|
||||||
|
|
||||||
class EmojiThumbnail final : public DynamicImage {
|
class EmojiThumbnail final : public DynamicImage {
|
||||||
public:
|
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;
|
std::shared_ptr<DynamicImage> clone() override;
|
||||||
|
|
||||||
|
@ -207,6 +211,8 @@ private:
|
||||||
const not_null<Data::Session*> _owner;
|
const not_null<Data::Session*> _owner;
|
||||||
const QString _data;
|
const QString _data;
|
||||||
std::unique_ptr<Ui::Text::CustomEmoji> _emoji;
|
std::unique_ptr<Ui::Text::CustomEmoji> _emoji;
|
||||||
|
Fn<bool()> _paused;
|
||||||
|
Fn<QColor()> _textColor;
|
||||||
QImage _frame;
|
QImage _frame;
|
||||||
|
|
||||||
};
|
};
|
||||||
|
@ -581,9 +587,13 @@ void IconThumbnail::subscribeToUpdates(Fn<void()> callback) {
|
||||||
|
|
||||||
EmojiThumbnail::EmojiThumbnail(
|
EmojiThumbnail::EmojiThumbnail(
|
||||||
not_null<Data::Session*> owner,
|
not_null<Data::Session*> owner,
|
||||||
const QString &data)
|
const QString &data,
|
||||||
|
Fn<bool()> paused,
|
||||||
|
Fn<QColor()> textColor)
|
||||||
: _owner(owner)
|
: _owner(owner)
|
||||||
, _data(data) {
|
, _data(data)
|
||||||
|
, _paused(std::move(paused))
|
||||||
|
, _textColor(std::move(textColor)) {
|
||||||
}
|
}
|
||||||
|
|
||||||
void EmojiThumbnail::subscribeToUpdates(Fn<void()> callback) {
|
void EmojiThumbnail::subscribeToUpdates(Fn<void()> callback) {
|
||||||
|
@ -598,7 +608,11 @@ void EmojiThumbnail::subscribeToUpdates(Fn<void()> callback) {
|
||||||
}
|
}
|
||||||
|
|
||||||
std::shared_ptr<DynamicImage> EmojiThumbnail::clone() {
|
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) {
|
QImage EmojiThumbnail::image(int size) {
|
||||||
|
@ -614,12 +628,16 @@ QImage EmojiThumbnail::image(int size) {
|
||||||
}
|
}
|
||||||
_frame.fill(Qt::transparent);
|
_frame.fill(Qt::transparent);
|
||||||
|
|
||||||
|
const auto esize = Text::AdjustCustomEmojiSize(
|
||||||
|
Emoji::GetSizeLarge() / style::DevicePixelRatio());
|
||||||
|
const auto eskip = (size - esize) / 2;
|
||||||
|
|
||||||
auto p = Painter(&_frame);
|
auto p = Painter(&_frame);
|
||||||
_emoji->paint(p, {
|
_emoji->paint(p, {
|
||||||
.textColor = st::windowBoldFg->c,
|
.textColor = _textColor ? _textColor() : st::windowBoldFg->c,
|
||||||
.now = crl::now(),
|
.now = crl::now(),
|
||||||
.position = QPoint(0, 0),
|
.position = QPoint(eskip, eskip),
|
||||||
.paused = false,
|
.paused = _paused && _paused(),
|
||||||
});
|
});
|
||||||
p.end();
|
p.end();
|
||||||
|
|
||||||
|
@ -665,8 +683,14 @@ std::shared_ptr<DynamicImage> MakeIconThumbnail(const style::icon &icon) {
|
||||||
|
|
||||||
std::shared_ptr<DynamicImage> MakeEmojiThumbnail(
|
std::shared_ptr<DynamicImage> MakeEmojiThumbnail(
|
||||||
not_null<Data::Session*> owner,
|
not_null<Data::Session*> owner,
|
||||||
const QString &data) {
|
const QString &data,
|
||||||
return std::make_shared<EmojiThumbnail>(owner, 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(
|
std::shared_ptr<DynamicImage> MakePhotoThumbnail(
|
||||||
|
|
|
@ -33,7 +33,9 @@ class DynamicImage;
|
||||||
const style::icon &icon);
|
const style::icon &icon);
|
||||||
[[nodiscard]] std::shared_ptr<DynamicImage> MakeEmojiThumbnail(
|
[[nodiscard]] std::shared_ptr<DynamicImage> MakeEmojiThumbnail(
|
||||||
not_null<Data::Session*> owner,
|
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(
|
[[nodiscard]] std::shared_ptr<DynamicImage> MakePhotoThumbnail(
|
||||||
not_null<PhotoData*> photo,
|
not_null<PhotoData*> photo,
|
||||||
FullMsgId fullId);
|
FullMsgId fullId);
|
||||||
|
|
|
@ -386,6 +386,8 @@ PRIVATE
|
||||||
ui/controls/send_as_button.h
|
ui/controls/send_as_button.h
|
||||||
ui/controls/send_button.cpp
|
ui/controls/send_button.cpp
|
||||||
ui/controls/send_button.h
|
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.cpp
|
||||||
ui/controls/swipe_handler.h
|
ui/controls/swipe_handler.h
|
||||||
ui/controls/swipe_handler_data.h
|
ui/controls/swipe_handler_data.h
|
||||||
|
|
Loading…
Add table
Reference in a new issue