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)
|
||||
? 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() {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -500,8 +500,8 @@ dialogsLoadMoreLoading: InfiniteRadialAnimation(defaultInfiniteRadialAnimation)
|
|||
}
|
||||
|
||||
dialogsSearchInHeight: 38px;
|
||||
dialogsSearchInPhotoSize: 26px;
|
||||
dialogsSearchInPhotoPadding: 12px;
|
||||
dialogsSearchInPhotoSize: 28px;
|
||||
dialogsSearchInPhotoPadding: 10px;
|
||||
dialogsSearchInSkip: 10px;
|
||||
dialogsSearchInNameTop: 9px;
|
||||
dialogsSearchInDownTop: 15px;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 §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
|
||||
|
||||
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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) {
|
||||
}
|
||||
|
|
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 {
|
||||
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(
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Add table
Reference in a new issue