Support vertical tabs somehow.

This commit is contained in:
John Preston 2025-05-23 21:12:03 +04:00
parent 72b57924b7
commit e0e69ce740
2 changed files with 469 additions and 13 deletions

View file

@ -16,21 +16,304 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "data/data_saved_sublist.h"
#include "data/data_session.h"
#include "data/data_thread.h"
#include "data/data_user.h"
#include "dialogs/dialogs_main_list.h"
#include "history/history.h"
#include "lang/lang_keys.h"
#include "main/main_session.h"
#include "ui/effects/ripple_animation.h"
#include "ui/text/text_utilities.h"
#include "ui/widgets/buttons.h"
#include "ui/widgets/discrete_sliders.h"
#include "ui/widgets/scroll_area.h"
#include "ui/widgets/shadow.h"
#include "ui/dynamic_image.h"
#include "ui/dynamic_thumbnails.h"
#include "window/window_session_controller.h"
#include "styles/style_chat.h"
namespace HistoryView {
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 &section : sections) {
const auto i = ranges::find(old, section.userpic, &Tab::userpic);
if (i != end(old)) {
_tabs.push_back(std::move(*i));
old.erase(i);
} else {
_tabs.push_back({ .userpic = std::move(section.userpic), });
}
_tabs.back().text = Ui::Text::String(
_st.nameStyle,
section.text,
kDefaultTextOptions,
_st.nameWidth);
}
for (const auto &was : old) {
if (was.subscribed) {
was.userpic->subscribeToUpdates(nullptr);
}
}
}
void VerticalSlider::setActiveSectionFast(int active) {
_active = active;
_activeTop.stop();
_activeHeight.stop();
}
void VerticalSlider::fitHeightToSections() {
auto top = 0;
for (auto &tab : _tabs) {
tab.top = top;
tab.height = _st.baseHeight + std::min(
_st.nameStyle.font->height * kMaxNameLines,
tab.text.countHeight(_st.nameWidth, true));
top += tab.height;
}
resize(_st.width, top);
}
int VerticalSlider::sectionsCount() const {
return int(_tabs.size());
}
int VerticalSlider::lookupSectionTop(int index) const {
Expects(index >= 0 && index < _tabs.size());
return _tabs[index].top;
}
VerticalSlider::Range VerticalSlider::getFinalActiveRange() const {
return (_active >= 0)
? Range{ _tabs[_active].top, _tabs[_active].height }
: Range();
}
void VerticalSlider::paintEvent(QPaintEvent *e) {
const auto finalRange = getFinalActiveRange();
const auto range = Range{
int(base::SafeRound(_activeTop.value(finalRange.top))),
int(base::SafeRound(_activeHeight.value(finalRange.height))),
};
auto p = QPainter(this);
auto clip = e->rect();
const auto drawRect = [&](QRect rect) {
_bar.paint(p, rect);
};
const auto nameLeft = (_st.width - _st.nameWidth) / 2;
for (auto &tab : _tabs) {
if (!clip.intersects(QRect(0, tab.top, width(), tab.height))) {
continue;
}
const auto divider = std::max(std::min(tab.height, range.height), 1);
const auto active = 1.
- std::clamp(
std::abs(range.top - tab.top) / float64(divider),
0.,
1.);
if (tab.ripple) {
const auto color = anim::color(
_st.rippleBg,
_st.rippleBgActive,
active);
tab.ripple->paint(p, 0, tab.top, width(), &color);
if (tab.ripple->empty()) {
tab.ripple.reset();
}
}
if (!tab.subscribed) {
tab.subscribed = true;
tab.userpic->subscribeToUpdates([=] { update(); });
}
const auto &image = tab.userpic->image(_st.userpicSize);
const auto userpicLeft = (width() - _st.userpicSize) / 2;
p.drawImage(userpicLeft, tab.top + _st.userpicTop, image);
p.setPen(anim::pen(_st.nameFg, _st.nameFgActive, active));
tab.text.draw(p, {
.position = QPoint(nameLeft, tab.top + _st.nameTop),
.outerWidth = width(),
.availableWidth = _st.nameWidth,
.align = style::al_top,
.paused = _paused && _paused(),
});
}
if (range.height > 0) {
const auto add = _st.barStroke / 2;
drawRect(myrtlrect(-add, range.top, _st.barStroke, range.height));
}
}
void VerticalSlider::timerEvent(QTimerEvent *e) {
activateCallback();
}
void VerticalSlider::startRipple(int index) {
if (!_st.ripple.showDuration) {
return;
}
auto &tab = _tabs[index];
if (!tab.ripple) {
auto mask = prepareRippleMask(index, tab);
tab.ripple = std::make_unique<Ui::RippleAnimation>(
_st.ripple,
std::move(mask),
[this] { update(); });
}
const auto point = mapFromGlobal(QCursor::pos());
tab.ripple->add(point - QPoint(0, tab.top));
}
QImage VerticalSlider::prepareRippleMask(int index, const Tab &tab) {
return Ui::RippleAnimation::RectMask(QSize(width(), tab.height));
}
int VerticalSlider::getIndexFromPosition(QPoint position) const {
const auto count = int(_tabs.size());
for (auto i = 0; i != count; ++i) {
const auto &tab = _tabs[i];
if (position.y() < tab.top + tab.height) {
return i;
}
}
return count - 1;
}
void VerticalSlider::mousePressEvent(QMouseEvent *e) {
for (auto i = 0, count = int(_tabs.size()); i != count; ++i) {
auto &tab = _tabs[i];
if (tab.top <= e->y() && e->y() < tab.top + tab.height) {
startRipple(i);
_pressed = i;
break;
}
}
}
void VerticalSlider::mouseReleaseEvent(QMouseEvent *e) {
const auto pressed = std::exchange(_pressed, -1);
if (pressed < 0) {
return;
}
const auto index = getIndexFromPosition(e->pos());
if (pressed < _tabs.size()) {
if (_tabs[pressed].ripple) {
_tabs[pressed].ripple->lastStop();
}
}
if (index == pressed) {
if (_active != index) {
_callbackAfterMs = crl::now() + _st.duration;
activateCallback();
const auto from = getFinalActiveRange();
_active = index;
const auto to = getFinalActiveRange();
const auto updater = [this] { update(); };
_activeTop.start(updater, from.top, to.top, _st.duration);
_activeHeight.start(
updater,
from.height,
to.height,
_st.duration);
}
}
}
void VerticalSlider::activateCallback() {
if (_timerId >= 0) {
killTimer(_timerId);
_timerId = -1;
}
auto ms = crl::now();
if (ms >= _callbackAfterMs) {
_sectionActivated.fire_copy(_active);
} else {
_timerId = startTimer(_callbackAfterMs - ms, Qt::PreciseTimer);
}
}
} // namespace
@ -47,6 +330,13 @@ SubsectionTabs::SubsectionTabs(
track();
refreshSlice();
setupHorizontal(parent);
dataChanged() | rpl::start_with_next([=] {
if (_loading) {
_loading = false;
refreshSlice();
}
}, _lifetime);
}
SubsectionTabs::~SubsectionTabs() {
@ -63,6 +353,8 @@ void SubsectionTabs::setupHorizontal(not_null<QWidget*> parent) {
if (!_shadow) {
_shadow = Ui::CreateChild<Ui::PlainShadow>(parent);
_shadow->show();
} else {
_shadow->raise();
}
const auto toggle = Ui::CreateChild<Ui::IconButton>(
@ -132,13 +424,6 @@ void SubsectionTabs::setupHorizontal(not_null<QWidget*> parent) {
}
}, _horizontal->lifetime());
dataChanged() | rpl::start_with_next([=] {
if (_loading) {
_loading = false;
refreshSlice();
}
}, _horizontal->lifetime());
_horizontal->sizeValue(
) | rpl::start_with_next([=](QSize size) {
const auto togglew = toggle->width();
@ -147,7 +432,11 @@ void SubsectionTabs::setupHorizontal(not_null<QWidget*> parent) {
}, scroll->lifetime());
_horizontal->paintRequest() | rpl::start_with_next([=](QRect clip) {
QPainter(_horizontal).fillRect(clip, st::windowBg);
QPainter(_horizontal).fillRect(
clip.intersected(
_horizontal->rect().marginsRemoved(
{ 0, 0, 0, st::lineWidth })),
st::windowBg);
}, _horizontal->lifetime());
_refreshed.events_starting_with_copy(
@ -254,8 +543,52 @@ void SubsectionTabs::setupVertical(not_null<QWidget*> parent) {
toggleModes();
});
toggle->move(0, 0);
const auto scroll = Ui::CreateChild<Ui::ScrollArea>(_vertical);
const auto scroll = Ui::CreateChild<Ui::ScrollArea>(
_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());
_vertical->sizeValue(
) | rpl::start_with_next([=](QSize size) {
@ -271,7 +604,90 @@ void SubsectionTabs::setupVertical(not_null<QWidget*> parent) {
_refreshed.events_starting_with_copy(
rpl::empty
) | rpl::start_with_next([=] {
_vertical->resize(std::max(toggle->width(), 0), 0);
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 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 scrollTop = scroll->scrollTop();
auto indexTop = tabs->lookupSectionTop(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) {
scrollSavingThread = _sectionsSlice[index];
scrollSavingShift = scrollTop - indexTop;
break;
}
indexTop = nextTop;
}
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 = scrollTop
- tabs->lookupSectionTop(index);
scrollSavingIndex = index;
break;
}
}
}
}
tabs->setSections(sections, paused);
tabs->fitHeightToSections();
tabs->setActiveSectionFast(activeIndex);
_sectionsSlice = _slice;
_vertical->resize(
std::max(toggle->width(), tabs->width()),
_vertical->height());
if (scrollSavingIndex >= 0) {
scroll->scrollToY(tabs->lookupSectionTop(scrollSavingIndex)
+ scrollSavingShift);
}
_scrollCheckRequests.fire({});
}, _vertical->lifetime());
}
@ -341,7 +757,7 @@ void SubsectionTabs::setBoundingRect(QRect boundingRect) {
_horizontal->height());
_shadow->setGeometry(
boundingRect.x(),
_horizontal->y() + _horizontal->height(),
_horizontal->y() + _horizontal->height() - st::lineWidth,
boundingRect.width(),
st::lineWidth);
} else {
@ -367,7 +783,7 @@ int SubsectionTabs::leftSkip() const {
}
int SubsectionTabs::topSkip() const {
return _horizontal ? _horizontal->height() : 0;
return _horizontal ? (_horizontal->height() - st::lineWidth) : 0;
}
void SubsectionTabs::raise() {

View file

@ -1284,3 +1284,43 @@ chatTabsSlider: SettingsSlider(defaultSettingsSlider) {
rippleBgActive: lightButtonBgOver;
ripple: defaultRippleAnimation;
}
ChatTabsVertical {
barStroke: pixels;
barRadius: pixels;
barFg: color;
nameStyle: TextStyle;
nameWidth: pixels;
nameTop: pixels;
nameFg: color;
nameFgActive: color;
userpicTop: pixels;
userpicSize: pixels;
baseHeight: pixels;
width: pixels;
ripple: RippleAnimation;
rippleBg: color;
rippleBgActive: color;
duration: int;
}
chatTabsVertical: ChatTabsVertical {
barStroke: 8px;
barRadius: 4px;
barFg: sliderBgActive;
nameStyle: TextStyle(defaultTextStyle) {
font: font(10px);
}
nameWidth: 46px;
nameTop: 46px;
nameFg: windowSubTextFg;
nameFgActive: lightButtonFg;
userpicTop: 8px;
userpicSize: 36px;
baseHeight: 56px;
width: 56px;
ripple: defaultRippleAnimation;
rippleBg: windowBgOver;
rippleBgActive: lightButtonBgOver;
duration: 150;
}