mirror of
https://github.com/AyuGram/AyuGramDesktop.git
synced 2026-08-06 03:42:56 +00:00
Added profile media tabs host and island strip.
This commit is contained in:
@@ -1234,6 +1234,14 @@ PRIVATE
|
||||
info/profile/info_profile_values.h
|
||||
info/profile/info_profile_widget.cpp
|
||||
info/profile/info_profile_widget.h
|
||||
info/profile/tabs/info_profile_tab_skeleton.cpp
|
||||
info/profile/tabs/info_profile_tab_skeleton.h
|
||||
info/profile/tabs/info_profile_tab_content.h
|
||||
info/profile/tabs/info_profile_tab_top_bar_bindings.h
|
||||
info/profile/tabs/info_profile_tabs_host.cpp
|
||||
info/profile/tabs/info_profile_tabs_host.h
|
||||
info/profile/tabs/info_profile_tabs_strip.cpp
|
||||
info/profile/tabs/info_profile_tabs_strip.h
|
||||
info/reactions_list/info_reactions_list_widget.cpp
|
||||
info/reactions_list/info_reactions_list_widget.h
|
||||
info/requests_list/info_requests_list_widget.cpp
|
||||
|
||||
@@ -63,6 +63,21 @@ defaultSubTabs: SubTabs {
|
||||
bg: windowBg;
|
||||
}
|
||||
|
||||
ProfileTabsStrip {
|
||||
height: pixels;
|
||||
margin: margins;
|
||||
skip: pixels;
|
||||
tabPadding: margins;
|
||||
activeSkip: pixels;
|
||||
style: TextStyle;
|
||||
fg: color;
|
||||
fgActive: color;
|
||||
bg: color;
|
||||
bgActive: color;
|
||||
ripple: RippleAnimation;
|
||||
duration: int;
|
||||
}
|
||||
|
||||
infoMediaHeaderFg: windowFg;
|
||||
|
||||
infoMediaSearch: SearchFieldRow {
|
||||
@@ -827,8 +842,39 @@ infoMediaSkip: 2px;
|
||||
infoMediaLeft: 3px;
|
||||
infoMediaMargin: margins(0px, 6px, 0px, 2px);
|
||||
infoMediaMinGridSize: 82px;
|
||||
infoMediaSkeletonMinHeight: 240px;
|
||||
infoMediaSkeletonRowHeight: 64px;
|
||||
infoMediaSkeletonRowPadding: 12px;
|
||||
infoMediaSkeletonBarHeight: 10px;
|
||||
infoMediaSkeletonFileThumb: 46px;
|
||||
infoMediaSkeletonLinkRow: 56px;
|
||||
infoMediaSkeletonLinkThumb: 32px;
|
||||
infoMediaSkeletonSoundSize: 42px;
|
||||
infoMediaTabsRightSkip: 13px;
|
||||
infoMediaGridSizeStep: 16px;
|
||||
|
||||
infoProfileTabsStrip: ProfileTabsStrip {
|
||||
height: 48px;
|
||||
margin: margins(8px, 6px, 8px, 6px);
|
||||
skip: 0px;
|
||||
tabPadding: margins(16px, 0px, 16px, 0px);
|
||||
activeSkip: 4px;
|
||||
style: semiboldTextStyle;
|
||||
fg: windowSubTextFg;
|
||||
fgActive: lightButtonFg;
|
||||
bg: windowBg;
|
||||
bgActive: lightButtonBgOver;
|
||||
ripple: RippleAnimation(defaultRippleAnimation) {
|
||||
color: lightButtonBgRipple;
|
||||
}
|
||||
duration: 200;
|
||||
}
|
||||
infoProfileTabsShadow: BoxShadow {
|
||||
blurRadius: 8px;
|
||||
offset: point(0px, 2px);
|
||||
opacity: 0.18;
|
||||
}
|
||||
|
||||
infoCommonGroupsMargin: margins(0px, 2px, 0px, 2px);
|
||||
infoCommonGroupsListItem: PeerListItem(defaultPeerListItem) {
|
||||
height: 52px;
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
/*
|
||||
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 "info/profile/tabs/info_profile_tab_top_bar_bindings.h"
|
||||
|
||||
class PeerData;
|
||||
|
||||
namespace Data {
|
||||
class ForumTopic;
|
||||
class SavedSublist;
|
||||
} // namespace Data
|
||||
|
||||
namespace Ui {
|
||||
class RpWidget;
|
||||
} // namespace Ui
|
||||
|
||||
namespace Info {
|
||||
class Controller;
|
||||
} // namespace Info
|
||||
|
||||
namespace Info::Profile {
|
||||
|
||||
struct MediaTabContext {
|
||||
not_null<Controller*> controller;
|
||||
not_null<PeerData*> peer;
|
||||
Data::ForumTopic *topic = nullptr;
|
||||
Data::SavedSublist *sublist = nullptr;
|
||||
PeerData *migrated = nullptr;
|
||||
QWidget *parent = nullptr;
|
||||
Fn<void(int ymin, int ymax)> scrollToRequest;
|
||||
Fn<void(int count)> onlineCountChanged;
|
||||
};
|
||||
|
||||
class MediaTabContent {
|
||||
public:
|
||||
virtual ~MediaTabContent() = default;
|
||||
|
||||
[[nodiscard]] virtual not_null<Ui::RpWidget*> widget() = 0;
|
||||
[[nodiscard]] virtual TabTopBarBindings topBarBindings() = 0;
|
||||
|
||||
virtual void deactivated() {
|
||||
}
|
||||
|
||||
virtual void setVisibleRegion(int top, int bottom) {
|
||||
}
|
||||
|
||||
virtual void setTopOverlay(int height) {
|
||||
}
|
||||
|
||||
virtual void saveScrollState(QByteArray &out) {
|
||||
}
|
||||
virtual void restoreScrollState(const QByteArray &in) {
|
||||
}
|
||||
};
|
||||
|
||||
struct MediaTabDescriptor {
|
||||
QString id;
|
||||
rpl::producer<QString> title;
|
||||
rpl::producer<bool> shown;
|
||||
Fn<std::unique_ptr<MediaTabContent>(MediaTabContext)> factory;
|
||||
};
|
||||
|
||||
} // namespace Info::Profile
|
||||
@@ -0,0 +1,154 @@
|
||||
/*
|
||||
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 "info/profile/tabs/info_profile_tab_skeleton.h"
|
||||
|
||||
#include "ui/effects/animations.h"
|
||||
#include "ui/effects/glare.h"
|
||||
#include "ui/painter.h"
|
||||
#include "ui/rp_widget.h"
|
||||
#include "styles/style_info.h"
|
||||
|
||||
namespace Info::Profile {
|
||||
namespace {
|
||||
|
||||
using Type = Storage::SharedMediaType;
|
||||
|
||||
void PaintGridSkeleton(QPainter &p, int width, int height) {
|
||||
const auto minSize = st::infoMediaMinGridSize;
|
||||
const auto columns = std::max(width / minSize, 1);
|
||||
const auto size = (width - (columns - 1)) / float64(columns);
|
||||
for (auto y = 0.; y < height; y += size + 1) {
|
||||
for (auto i = 0; i != columns; ++i) {
|
||||
p.drawRect(QRectF(i * (size + 1), y, size, size));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void PaintRowsSkeleton(
|
||||
QPainter &p,
|
||||
int width,
|
||||
int height,
|
||||
int rowHeight,
|
||||
int thumbSize,
|
||||
bool round,
|
||||
int lines) {
|
||||
const auto padding = st::infoMediaSkeletonRowPadding;
|
||||
const auto bar = st::infoMediaSkeletonBarHeight;
|
||||
const auto widths = std::array{ 3, 2, 1 };
|
||||
for (auto y = 0; y < height; y += rowHeight) {
|
||||
const auto thumbTop = y + (rowHeight - thumbSize) / 2;
|
||||
if (round) {
|
||||
p.drawEllipse(padding, thumbTop, thumbSize, thumbSize);
|
||||
} else {
|
||||
p.drawRoundedRect(
|
||||
QRect(padding, thumbTop, thumbSize, thumbSize),
|
||||
st::roundRadiusSmall,
|
||||
st::roundRadiusSmall);
|
||||
}
|
||||
const auto left = padding + thumbSize + padding;
|
||||
const auto available = width - left - padding;
|
||||
if (available <= 0) {
|
||||
continue;
|
||||
}
|
||||
const auto linesHeight = lines * bar + (lines - 1) * (bar / 2);
|
||||
auto barTop = y + (rowHeight - linesHeight) / 2;
|
||||
for (auto line = 0; line != lines; ++line) {
|
||||
p.drawRoundedRect(
|
||||
left,
|
||||
barTop,
|
||||
available * widths[line % widths.size()] / 5,
|
||||
bar,
|
||||
bar / 2.,
|
||||
bar / 2.);
|
||||
barTop += bar + (bar / 2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
object_ptr<Ui::RpWidget> CreateTabSkeleton(
|
||||
not_null<QWidget*> parent,
|
||||
Storage::SharedMediaType type) {
|
||||
auto widget = object_ptr<Ui::RpWidget>(parent);
|
||||
const auto raw = widget.data();
|
||||
raw->setAttribute(Qt::WA_TransparentForMouseEvents);
|
||||
|
||||
struct State {
|
||||
Ui::GlareEffect glare;
|
||||
};
|
||||
const auto state = raw->lifetime().make_state<State>();
|
||||
|
||||
raw->paintRequest(
|
||||
) | rpl::on_next([=] {
|
||||
auto p = QPainter(raw);
|
||||
auto hq = PainterHighQualityEnabler(p);
|
||||
p.setPen(Qt::NoPen);
|
||||
p.setBrush(st::windowBgOver);
|
||||
const auto width = raw->width();
|
||||
const auto height = raw->height();
|
||||
switch (type) {
|
||||
case Type::Photo:
|
||||
case Type::Video:
|
||||
case Type::GIF:
|
||||
PaintGridSkeleton(p, width, height);
|
||||
break;
|
||||
case Type::File:
|
||||
PaintRowsSkeleton(
|
||||
p,
|
||||
width,
|
||||
height,
|
||||
st::infoMediaSkeletonRowHeight,
|
||||
st::infoMediaSkeletonFileThumb,
|
||||
false,
|
||||
3);
|
||||
break;
|
||||
case Type::Link:
|
||||
PaintRowsSkeleton(
|
||||
p,
|
||||
width,
|
||||
height,
|
||||
st::infoMediaSkeletonLinkRow,
|
||||
st::infoMediaSkeletonLinkThumb,
|
||||
false,
|
||||
3);
|
||||
break;
|
||||
default:
|
||||
PaintRowsSkeleton(
|
||||
p,
|
||||
width,
|
||||
height,
|
||||
st::infoMediaSkeletonLinkRow,
|
||||
st::infoMediaSkeletonSoundSize,
|
||||
true,
|
||||
2);
|
||||
break;
|
||||
}
|
||||
auto &glare = state->glare;
|
||||
if (glare.glare.birthTime) {
|
||||
const auto progress = glare.progress(crl::now());
|
||||
const auto x = (-glare.width)
|
||||
+ (width + glare.width * 2) * progress;
|
||||
p.drawTiledPixmap(x, 0, glare.width, height, glare.pixmap, 0, 0);
|
||||
}
|
||||
}, raw->lifetime());
|
||||
|
||||
raw->widthValue(
|
||||
) | rpl::on_next([=](int width) {
|
||||
state->glare.width = width;
|
||||
state->glare.validate(
|
||||
st::windowBg->c,
|
||||
[=] { raw->update(); },
|
||||
crl::time(1000),
|
||||
crl::time(1000));
|
||||
}, raw->lifetime());
|
||||
|
||||
return widget;
|
||||
}
|
||||
|
||||
} // namespace Info::Profile
|
||||
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "base/object_ptr.h"
|
||||
#include "storage/storage_shared_media.h"
|
||||
|
||||
namespace Ui {
|
||||
class RpWidget;
|
||||
} // namespace Ui
|
||||
|
||||
namespace Info::Profile {
|
||||
|
||||
[[nodiscard]] object_ptr<Ui::RpWidget> CreateTabSkeleton(
|
||||
not_null<QWidget*> parent,
|
||||
Storage::SharedMediaType type);
|
||||
|
||||
} // namespace Info::Profile
|
||||
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
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 "info/info_wrap_widget.h"
|
||||
|
||||
namespace Ui::Menu {
|
||||
struct MenuCallback;
|
||||
} // namespace Ui::Menu
|
||||
|
||||
namespace Info::Profile {
|
||||
|
||||
struct TabTopBarBindings {
|
||||
rpl::producer<TextWithEntities> title;
|
||||
rpl::producer<TextWithEntities> subtitle;
|
||||
Fn<void(const Ui::Menu::MenuCallback&)> fillMenu;
|
||||
rpl::producer<SelectedItems> selectedItems;
|
||||
rpl::producer<bool> searchEnabledByContent;
|
||||
Fn<void(SelectionAction)> selectionAction;
|
||||
Fn<void(QString)> applySearchQuery;
|
||||
};
|
||||
|
||||
} // namespace Info::Profile
|
||||
@@ -0,0 +1,513 @@
|
||||
/*
|
||||
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 "info/profile/tabs/info_profile_tabs_host.h"
|
||||
|
||||
#include "info/profile/tabs/info_profile_tabs_strip.h"
|
||||
#include "ui/widgets/scroll_area.h"
|
||||
#include "ui/effects/slide_animation.h"
|
||||
#include "ui/painter.h"
|
||||
#include "ui/ui_utility.h"
|
||||
#include "styles/style_basic.h"
|
||||
#include "styles/style_info.h"
|
||||
|
||||
namespace Info::Profile {
|
||||
|
||||
TabsHost::TabsHost(not_null<QWidget*> parent, Descriptor descriptor)
|
||||
: RpWidget(parent)
|
||||
, _context(descriptor.context)
|
||||
, _tabs(std::move(descriptor.tabs))
|
||||
, _strip(Ui::CreateChild<TabsStrip>(this, st::infoProfileTabsStrip))
|
||||
, _stripWeak(_strip)
|
||||
, _body(Ui::CreateChild<Ui::RpWidget>(this)) {
|
||||
_strip->show();
|
||||
_body->show();
|
||||
if (_tabs.empty()) {
|
||||
return;
|
||||
}
|
||||
_stripTitles.assign(_tabs.size(), QString());
|
||||
_tabsShown.assign(_tabs.size(), false);
|
||||
wireStripTitles();
|
||||
wireTabsVisibility();
|
||||
_strip->activated(
|
||||
) | rpl::on_next([this](const QString &id) {
|
||||
_userChosenTab = true;
|
||||
_pendingRestoreId = QString();
|
||||
if (id == _activeId) {
|
||||
// The viewport filler grows after the first scroll collapses
|
||||
// the cover, so settle with a second queued pass.
|
||||
const auto fire = [this] {
|
||||
_scrollToRequests.fire({ 0, -1 });
|
||||
};
|
||||
fire();
|
||||
InvokeQueued(this, fire);
|
||||
} else {
|
||||
activateTab(id);
|
||||
}
|
||||
}, lifetime());
|
||||
|
||||
_body->heightValue(
|
||||
) | rpl::on_next([this](int) {
|
||||
scheduleHeightSync();
|
||||
}, _body->lifetime());
|
||||
rpl::merge(
|
||||
_strip->heightValue() | rpl::to_empty,
|
||||
_strip->naturalWidthValue() | rpl::to_empty
|
||||
) | rpl::on_next([this] {
|
||||
scheduleHeightSync();
|
||||
}, lifetime());
|
||||
}
|
||||
|
||||
void TabsHost::scheduleBodySync() {
|
||||
if (_bodySyncQueued) {
|
||||
return;
|
||||
}
|
||||
_bodySyncQueued = true;
|
||||
InvokeQueued(this, [=] {
|
||||
if (_bodySyncQueued) {
|
||||
syncBodyNow();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void TabsHost::syncBodyNow() {
|
||||
_bodySyncQueued = false;
|
||||
const auto active = _activeTab.current();
|
||||
if (!active) {
|
||||
return;
|
||||
}
|
||||
const auto widget = active->widget();
|
||||
if (!widget->isHidden() && _body->height() != widget->height()) {
|
||||
_body->resize(_body->width(), widget->height());
|
||||
}
|
||||
}
|
||||
|
||||
void TabsHost::scheduleHeightSync() {
|
||||
if (_heightSyncQueued) {
|
||||
return;
|
||||
}
|
||||
_heightSyncQueued = true;
|
||||
InvokeQueued(this, [=] {
|
||||
if (_heightSyncQueued) {
|
||||
syncHeightNow();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void TabsHost::syncHeightNow() {
|
||||
_heightSyncQueued = false;
|
||||
resizeToWidth(width());
|
||||
}
|
||||
|
||||
TabsHost::~TabsHost() {
|
||||
// Lists notify their delegates while being destroyed, so the tab
|
||||
// widgets must die before the adapters owning those delegates.
|
||||
delete base::take(_body);
|
||||
}
|
||||
|
||||
void TabsHost::wireStripTitles() {
|
||||
for (auto i = 0; i != int(_tabs.size()); ++i) {
|
||||
rpl::duplicate(
|
||||
_tabs[i].title
|
||||
) | rpl::on_next([this, i](QString text) {
|
||||
if (_stripTitles[i] != text) {
|
||||
_stripTitles[i] = std::move(text);
|
||||
syncStripTitles();
|
||||
}
|
||||
}, lifetime());
|
||||
}
|
||||
}
|
||||
|
||||
void TabsHost::wireTabsVisibility() {
|
||||
for (auto i = 0; i != int(_tabs.size()); ++i) {
|
||||
rpl::duplicate(
|
||||
_tabs[i].shown
|
||||
) | rpl::on_next([this, i](bool shown) {
|
||||
if (_tabsShown[i] != shown) {
|
||||
_tabsShown[i] = shown;
|
||||
syncStripTitles();
|
||||
if (shown
|
||||
&& !_pendingRestoreId.isEmpty()
|
||||
&& (_tabs[i].id == _pendingRestoreId)) {
|
||||
restoreActiveTab(base::take(_pendingRestoreId));
|
||||
}
|
||||
ensureActiveVisible();
|
||||
scheduleHeightSync();
|
||||
}
|
||||
}, lifetime());
|
||||
}
|
||||
}
|
||||
|
||||
void TabsHost::syncStripTitles() {
|
||||
auto stripTabs = std::vector<StripTab>();
|
||||
stripTabs.reserve(_tabs.size());
|
||||
for (auto i = 0; i != int(_tabs.size()); ++i) {
|
||||
if (!_tabsShown[i]) {
|
||||
continue;
|
||||
}
|
||||
stripTabs.push_back({
|
||||
.id = _tabs[i].id,
|
||||
.text = _stripTitles[i],
|
||||
});
|
||||
}
|
||||
_strip->setTabs(std::move(stripTabs));
|
||||
if (!_activeId.isEmpty()) {
|
||||
const auto it = ranges::find(
|
||||
_tabs,
|
||||
_activeId,
|
||||
&MediaTabDescriptor::id);
|
||||
if (it != end(_tabs) && _tabsShown[it - begin(_tabs)]) {
|
||||
_strip->setActiveTab(_activeId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void TabsHost::ensureActiveVisible() {
|
||||
const auto firstVisible = [&] {
|
||||
for (auto i = 0; i != int(_tabs.size()); ++i) {
|
||||
if (_tabsShown[i]) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}();
|
||||
const auto activeIndex = _activeId.isEmpty()
|
||||
? -1
|
||||
: int(ranges::find(_tabs, _activeId, &MediaTabDescriptor::id)
|
||||
- begin(_tabs));
|
||||
const auto activeVisible = (activeIndex >= 0)
|
||||
&& (activeIndex < int(_tabs.size()))
|
||||
&& _tabsShown[activeIndex];
|
||||
if (activeVisible
|
||||
&& (_userChosenTab || activeIndex == firstVisible)) {
|
||||
return;
|
||||
}
|
||||
if (firstVisible >= 0) {
|
||||
if (activeIndex != firstVisible) {
|
||||
activateTab(_tabs[firstVisible].id);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (!_activeId.isEmpty()) {
|
||||
if (const auto previous = _contents[_activeId].get()) {
|
||||
previous->deactivated();
|
||||
previous->widget()->hide();
|
||||
}
|
||||
_activeId = QString();
|
||||
_activeTab = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
void TabsHost::restoreActiveTab(const QString &id) {
|
||||
const auto it = ranges::find(_tabs, id, &MediaTabDescriptor::id);
|
||||
if (it == end(_tabs)) {
|
||||
return;
|
||||
} else if (_tabsShown[it - begin(_tabs)]) {
|
||||
_userChosenTab = true;
|
||||
activateTab(id, false);
|
||||
} else {
|
||||
_pendingRestoreId = id;
|
||||
}
|
||||
}
|
||||
|
||||
QRect TabsHost::bodyGeometry() const {
|
||||
return _body->geometry();
|
||||
}
|
||||
|
||||
Fn<void()> TabsHost::prepareSwitch(bool toNextTab) {
|
||||
if (_activeId.isEmpty()) {
|
||||
return nullptr;
|
||||
}
|
||||
const auto active = int(ranges::find(
|
||||
_tabs,
|
||||
_activeId,
|
||||
&MediaTabDescriptor::id) - begin(_tabs));
|
||||
if (active >= int(_tabs.size())) {
|
||||
return nullptr;
|
||||
}
|
||||
const auto delta = toNextTab ? 1 : -1;
|
||||
for (auto i = active + delta
|
||||
; i >= 0 && i < int(_tabs.size())
|
||||
; i += delta) {
|
||||
if (!_tabsShown[i]) {
|
||||
continue;
|
||||
}
|
||||
return crl::guard(this, [=, id = _tabs[i].id] {
|
||||
_userChosenTab = true;
|
||||
_pendingRestoreId = QString();
|
||||
activateTab(id);
|
||||
});
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void TabsHost::activateTab(const QString &id, bool animated) {
|
||||
if (_activeId == id) {
|
||||
_strip->setActiveTab(id);
|
||||
return;
|
||||
}
|
||||
const auto it = ranges::find(_tabs, id, &MediaTabDescriptor::id);
|
||||
if (it == end(_tabs) || !_tabsShown[it - begin(_tabs)]) {
|
||||
return;
|
||||
}
|
||||
_strip->setActiveTab(id);
|
||||
|
||||
auto &cached = _contents[id];
|
||||
const auto created = !cached;
|
||||
if (created) {
|
||||
auto context = _context;
|
||||
context.parent = _body;
|
||||
context.scrollToRequest = [weak = base::make_weak(this), id](
|
||||
int ymin,
|
||||
int ymax) {
|
||||
const auto that = weak.get();
|
||||
if (!that) {
|
||||
return;
|
||||
}
|
||||
const auto i = that->_contents.find(id);
|
||||
if (i == end(that->_contents)) {
|
||||
return;
|
||||
}
|
||||
// Flush the queued height coalescers, otherwise the exact
|
||||
// scroll below clamps to a stale scroll range.
|
||||
that->syncBodyNow();
|
||||
that->syncHeightNow();
|
||||
const auto widget = i->second->widget();
|
||||
const auto top = Ui::MapFrom(
|
||||
that,
|
||||
widget.get(),
|
||||
QPoint(0, ymin)).y();
|
||||
that->_scrollToRequests.fire({
|
||||
top,
|
||||
(ymax < 0) ? -1 : (top + (ymax - ymin)),
|
||||
});
|
||||
};
|
||||
cached = it->factory(std::move(context));
|
||||
}
|
||||
const auto previousId = _activeId;
|
||||
const auto previousIndex = previousId.isEmpty()
|
||||
? -1
|
||||
: int(ranges::find(_tabs, previousId, &MediaTabDescriptor::id)
|
||||
- begin(_tabs));
|
||||
_activeId = id;
|
||||
|
||||
const auto previous = previousId.isEmpty()
|
||||
? nullptr
|
||||
: _contents[previousId].get();
|
||||
if (previous) {
|
||||
if (_visibleTop >= 0) {
|
||||
_tabScrollTops[previousId] = _visibleTop;
|
||||
} else {
|
||||
_tabScrollTops.remove(previousId);
|
||||
}
|
||||
_keepMinHeight = std::max(_visibleBottom, 0);
|
||||
previous->deactivated();
|
||||
}
|
||||
|
||||
const auto active = cached.get();
|
||||
const auto widget = active->widget();
|
||||
const auto raw = widget.get();
|
||||
if (created) {
|
||||
if (raw->parentWidget() != _body) {
|
||||
raw->setParent(_body);
|
||||
}
|
||||
_body->widthValue(
|
||||
) | rpl::on_next([raw](int newWidth) {
|
||||
if (!raw->isHidden()) {
|
||||
raw->resizeToWidth(newWidth);
|
||||
}
|
||||
}, raw->lifetime());
|
||||
raw->heightValue(
|
||||
) | rpl::on_next([this, raw](int) {
|
||||
if (!raw->isHidden()) {
|
||||
scheduleBodySync();
|
||||
}
|
||||
}, raw->lifetime());
|
||||
}
|
||||
raw->show();
|
||||
raw->resizeToWidth(_body->width());
|
||||
_body->resize(_body->width(), raw->height());
|
||||
|
||||
_activeTab = active;
|
||||
pushViewportToActive();
|
||||
|
||||
if (previous) {
|
||||
auto wasCache = QPixmap();
|
||||
if (animated
|
||||
&& _visibleBottom > _visibleTop
|
||||
&& _body->width() > 0) {
|
||||
const auto part = bodyVisibleRect().intersected(
|
||||
previous->widget()->rect());
|
||||
if (!part.isEmpty()) {
|
||||
wasCache = Ui::GrabWidget(previous->widget(), part);
|
||||
}
|
||||
}
|
||||
if (_visibleTop >= 0) {
|
||||
const auto i = _tabScrollTops.find(id);
|
||||
const auto restore = (i != end(_tabScrollTops))
|
||||
? i->second
|
||||
: 0;
|
||||
if (restore != _visibleTop) {
|
||||
const auto fire = [this, restore] {
|
||||
_scrollToRequests.fire({ restore, -1 });
|
||||
};
|
||||
fire();
|
||||
InvokeQueued(this, fire);
|
||||
}
|
||||
}
|
||||
if (animated) {
|
||||
startSlideAnimation(
|
||||
std::move(wasCache),
|
||||
active,
|
||||
previousIndex > int(it - begin(_tabs)));
|
||||
}
|
||||
previous->widget()->hide();
|
||||
}
|
||||
}
|
||||
|
||||
QRect TabsHost::bodyVisibleRect() const {
|
||||
const auto bodyTop = _stripHeight;
|
||||
const auto top = std::max(_visibleTop - bodyTop, 0);
|
||||
const auto bottom = std::min(
|
||||
_visibleBottom - bodyTop,
|
||||
std::max(_body->height(), height() - bodyTop));
|
||||
return QRect(
|
||||
0,
|
||||
top,
|
||||
_body->width(),
|
||||
std::max(bottom - top, 1));
|
||||
}
|
||||
|
||||
void TabsHost::startSlideAnimation(
|
||||
QPixmap wasCache,
|
||||
not_null<MediaTabContent*> now,
|
||||
bool slideLeft) {
|
||||
if (_visibleBottom <= _visibleTop || _body->width() <= 0) {
|
||||
return;
|
||||
}
|
||||
const auto rect = bodyVisibleRect();
|
||||
const auto part = rect.intersected(now->widget()->rect());
|
||||
auto nowCache = part.isEmpty()
|
||||
? QPixmap()
|
||||
: Ui::GrabWidget(now->widget(), part);
|
||||
if (wasCache.isNull() || nowCache.isNull()) {
|
||||
return;
|
||||
}
|
||||
_slideRect = rect.translated(0, _stripHeight);
|
||||
_slideAnimation = std::make_unique<Ui::SlideAnimation>();
|
||||
_slideAnimation->setSnapshots(
|
||||
std::move(wasCache),
|
||||
std::move(nowCache));
|
||||
_slideAnimation->start(slideLeft, [=] {
|
||||
if (_slideAnimation && !_slideAnimation->animating()) {
|
||||
_slideAnimation = nullptr;
|
||||
_body->show();
|
||||
}
|
||||
update();
|
||||
}, st::slideDuration);
|
||||
_body->hide();
|
||||
update();
|
||||
}
|
||||
|
||||
void TabsHost::paintEvent(QPaintEvent *e) {
|
||||
if (!_slideAnimation) {
|
||||
return;
|
||||
}
|
||||
auto p = QPainter(this);
|
||||
p.fillRect(_slideRect, st::windowBg);
|
||||
_slideAnimation->paintFrame(
|
||||
p,
|
||||
_slideRect.x(),
|
||||
_slideRect.y(),
|
||||
width());
|
||||
}
|
||||
|
||||
void TabsHost::pushViewportToActive() {
|
||||
if (const auto active = _activeTab.current()) {
|
||||
active->setTopOverlay((_visibleTop >= 0) ? _stripHeight : 0);
|
||||
active->setVisibleRegion(
|
||||
_visibleTop - _stripHeight,
|
||||
_visibleBottom - _stripHeight);
|
||||
}
|
||||
}
|
||||
|
||||
rpl::producer<MediaTabContent*> TabsHost::activeTabValue() const {
|
||||
return _activeTab.value();
|
||||
}
|
||||
|
||||
rpl::producer<Ui::ScrollToRequest> TabsHost::scrollToRequests() const {
|
||||
return _scrollToRequests.events();
|
||||
}
|
||||
|
||||
rpl::producer<TabTopBarBindings> TabsHost::activeTabBindings() const {
|
||||
return _activeTab.value(
|
||||
) | rpl::map([](MediaTabContent *tab) {
|
||||
return tab ? tab->topBarBindings() : TabTopBarBindings();
|
||||
});
|
||||
}
|
||||
|
||||
not_null<Ui::RpWidget*> TabsHost::stripWidget() const {
|
||||
return _strip;
|
||||
}
|
||||
|
||||
void TabsHost::returnStrip() {
|
||||
const auto strip = _stripWeak.get();
|
||||
if (!strip) {
|
||||
return;
|
||||
}
|
||||
if (strip->parentWidget() != this) {
|
||||
strip->setParent(this);
|
||||
strip->show();
|
||||
}
|
||||
resizeToWidth(width());
|
||||
}
|
||||
|
||||
void TabsHost::setVisibleRegion(int top, int bottom) {
|
||||
if (_visibleTop == top && _visibleBottom == bottom) {
|
||||
return;
|
||||
}
|
||||
const auto heightChanged = ((_visibleBottom - _visibleTop)
|
||||
!= (bottom - top));
|
||||
_visibleTop = top;
|
||||
_visibleBottom = bottom;
|
||||
pushViewportToActive();
|
||||
if (heightChanged) {
|
||||
const auto want = std::max(
|
||||
_stripHeight + _body->height(),
|
||||
bottom - top);
|
||||
if (want != height()) {
|
||||
scheduleHeightSync();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int TabsHost::resizeGetHeight(int newWidth) {
|
||||
if (!ranges::contains(_tabsShown, true)) {
|
||||
return 0;
|
||||
}
|
||||
if (const auto strip = _stripWeak.get()
|
||||
; strip && strip->parentWidget() == this) {
|
||||
const auto stripWidth = std::min(strip->naturalWidth(), newWidth);
|
||||
strip->resizeToWidth(stripWidth);
|
||||
strip->moveToLeft((newWidth - stripWidth) / 2, 0);
|
||||
_stripHeight = strip->height();
|
||||
}
|
||||
const auto bodyTop = _stripHeight;
|
||||
_body->resizeToWidth(std::max(newWidth, 1));
|
||||
_body->moveToLeft(0, bodyTop);
|
||||
|
||||
const auto natural = bodyTop + _body->height();
|
||||
const auto viewport = _visibleBottom - _visibleTop;
|
||||
if (_keepMinHeight
|
||||
&& ((natural >= _keepMinHeight)
|
||||
|| (_visibleBottom <= std::max(natural, viewport)))) {
|
||||
_keepMinHeight = 0;
|
||||
}
|
||||
return std::max({ natural, viewport, _keepMinHeight });
|
||||
}
|
||||
|
||||
} // namespace Info::Profile
|
||||
@@ -0,0 +1,100 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "base/object_ptr.h"
|
||||
#include "base/flat_map.h"
|
||||
#include "info/profile/tabs/info_profile_tab_content.h"
|
||||
#include "ui/rp_widget.h"
|
||||
|
||||
namespace Ui {
|
||||
class RpWidget;
|
||||
class SlideAnimation;
|
||||
struct ScrollToRequest;
|
||||
} // namespace Ui
|
||||
|
||||
namespace Info::Profile {
|
||||
|
||||
class TabsStrip;
|
||||
|
||||
class TabsHost final : public Ui::RpWidget {
|
||||
public:
|
||||
struct Descriptor {
|
||||
MediaTabContext context;
|
||||
std::vector<MediaTabDescriptor> tabs;
|
||||
};
|
||||
|
||||
TabsHost(not_null<QWidget*> parent, Descriptor descriptor);
|
||||
~TabsHost();
|
||||
|
||||
[[nodiscard]] rpl::producer<MediaTabContent*> activeTabValue() const;
|
||||
[[nodiscard]] rpl::producer<TabTopBarBindings> activeTabBindings() const;
|
||||
[[nodiscard]] rpl::producer<Ui::ScrollToRequest> scrollToRequests() const;
|
||||
|
||||
[[nodiscard]] not_null<Ui::RpWidget*> stripWidget() const;
|
||||
void returnStrip();
|
||||
void setVisibleRegion(int top, int bottom);
|
||||
|
||||
[[nodiscard]] QString activeId() const {
|
||||
return _activeId;
|
||||
}
|
||||
void activateTab(const QString &id, bool animated = true);
|
||||
void restoreActiveTab(const QString &id);
|
||||
|
||||
[[nodiscard]] QRect bodyGeometry() const;
|
||||
[[nodiscard]] Fn<void()> prepareSwitch(bool toNextTab);
|
||||
|
||||
protected:
|
||||
int resizeGetHeight(int newWidth) override;
|
||||
void paintEvent(QPaintEvent *e) override;
|
||||
|
||||
private:
|
||||
void wireStripTitles();
|
||||
void wireTabsVisibility();
|
||||
void syncStripTitles();
|
||||
void ensureActiveVisible();
|
||||
void pushViewportToActive();
|
||||
void scheduleBodySync();
|
||||
void scheduleHeightSync();
|
||||
void syncBodyNow();
|
||||
void syncHeightNow();
|
||||
[[nodiscard]] QRect bodyVisibleRect() const;
|
||||
void startSlideAnimation(
|
||||
QPixmap wasCache,
|
||||
not_null<MediaTabContent*> now,
|
||||
bool slideLeft);
|
||||
|
||||
const MediaTabContext _context;
|
||||
std::vector<MediaTabDescriptor> _tabs;
|
||||
std::vector<QString> _stripTitles;
|
||||
std::vector<bool> _tabsShown;
|
||||
|
||||
TabsStrip *_strip = nullptr;
|
||||
base::weak_qptr<TabsStrip> _stripWeak;
|
||||
Ui::RpWidget *_body = nullptr;
|
||||
int _stripHeight = 0;
|
||||
int _visibleTop = 0;
|
||||
int _visibleBottom = 0;
|
||||
|
||||
std::unique_ptr<Ui::SlideAnimation> _slideAnimation;
|
||||
QRect _slideRect;
|
||||
|
||||
base::flat_map<QString, std::unique_ptr<MediaTabContent>> _contents;
|
||||
base::flat_map<QString, int> _tabScrollTops;
|
||||
rpl::event_stream<Ui::ScrollToRequest> _scrollToRequests;
|
||||
QString _activeId;
|
||||
QString _pendingRestoreId;
|
||||
bool _userChosenTab = false;
|
||||
bool _bodySyncQueued = false;
|
||||
bool _heightSyncQueued = false;
|
||||
int _keepMinHeight = 0;
|
||||
rpl::variable<MediaTabContent*> _activeTab = nullptr;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Info::Profile
|
||||
@@ -0,0 +1,442 @@
|
||||
/*
|
||||
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 "info/profile/tabs/info_profile_tabs_strip.h"
|
||||
|
||||
#include "ui/effects/animation_value.h"
|
||||
#include "ui/effects/animation_value_f.h"
|
||||
#include "ui/effects/ripple_animation.h"
|
||||
#include "ui/painter.h"
|
||||
#include "ui/rect.h"
|
||||
#include "ui/ui_utility.h"
|
||||
#include "styles/style_basic.h"
|
||||
#include "styles/style_chat_helpers.h"
|
||||
#include "styles/style_info.h"
|
||||
|
||||
#include <QApplication>
|
||||
|
||||
namespace Info::Profile {
|
||||
namespace {
|
||||
|
||||
void PaintIslandOutline(
|
||||
QPainter &p,
|
||||
const QRectF &island,
|
||||
float64 radius,
|
||||
const style::color &bg) {
|
||||
const auto light = (bg->c.lightness() >= 128);
|
||||
const auto width = light ? (0.6 * st::lineWidth) : double(st::lineWidth);
|
||||
if (light) {
|
||||
p.setPen(QPen(QColor(0, 0, 0, 20), width));
|
||||
} else {
|
||||
auto grad = QLinearGradient(0, island.top(), 0, island.bottom());
|
||||
grad.setColorAt(0., QColor(255, 255, 255, 40));
|
||||
grad.setColorAt(0.5, QColor(255, 255, 255, 0));
|
||||
grad.setColorAt(1., QColor(255, 255, 255, 20));
|
||||
p.setPen(QPen(QBrush(grad), width));
|
||||
}
|
||||
p.setBrush(Qt::NoBrush);
|
||||
const auto half = 0.5 * width;
|
||||
const auto stroke = light
|
||||
? island.marginsAdded(QMarginsF(half, half, half, half))
|
||||
: island.adjusted(half, half, -half, -half);
|
||||
const auto strokeRadius = light ? (radius + half) : (radius - half);
|
||||
p.drawRoundedRect(stroke, strokeRadius, strokeRadius);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TabsStrip::TabsStrip(QWidget *parent, const style::ProfileTabsStrip &st)
|
||||
: RpWidget(parent)
|
||||
, _st(st)
|
||||
, _shadow(st::infoProfileTabsShadow) {
|
||||
setObjectName(u"profileTabsStrip"_q);
|
||||
setMouseTracking(true);
|
||||
}
|
||||
|
||||
void TabsStrip::setTabs(std::vector<StripTab> tabs) {
|
||||
const auto activeId = (_active >= 0 && _active < int(_buttons.size()))
|
||||
? _buttons[_active].tab.id
|
||||
: QString();
|
||||
setSelected(-1);
|
||||
const auto pillHeight = _st.height
|
||||
- _st.margin.top()
|
||||
- _st.margin.bottom();
|
||||
auto buttons = std::vector<Button>();
|
||||
buttons.reserve(tabs.size());
|
||||
_active = -1;
|
||||
_wasActive = -1;
|
||||
auto x = 0;
|
||||
for (auto &tab : tabs) {
|
||||
Assert(!tab.id.isEmpty());
|
||||
|
||||
auto button = Button();
|
||||
button.text.setText(_st.style, tab.text);
|
||||
const auto width = _st.tabPadding.left()
|
||||
+ button.text.maxWidth()
|
||||
+ _st.tabPadding.right();
|
||||
button.geometry = QRect(x, 0, width, pillHeight);
|
||||
if (tab.id == activeId) {
|
||||
_active = int(buttons.size());
|
||||
}
|
||||
button.tab = std::move(tab);
|
||||
buttons.push_back(std::move(button));
|
||||
x += width;
|
||||
}
|
||||
_buttons = std::move(buttons);
|
||||
_fullWidth = x;
|
||||
_activeAnimation.stop();
|
||||
if (_active >= 0) {
|
||||
_activeFrom = _activeTo = highlightRect(_active);
|
||||
}
|
||||
setNaturalWidth(_buttons.empty()
|
||||
? 0
|
||||
: (_st.margin.left()
|
||||
+ _st.skip
|
||||
+ _fullWidth
|
||||
+ _st.skip
|
||||
+ _st.margin.right()));
|
||||
resizeToWidth(width());
|
||||
update();
|
||||
}
|
||||
|
||||
void TabsStrip::setActiveTab(const QString &id) {
|
||||
if (id.isEmpty()) {
|
||||
setActive(-1);
|
||||
return;
|
||||
}
|
||||
const auto i = ranges::find(
|
||||
_buttons,
|
||||
id,
|
||||
[](const Button &button) { return button.tab.id; });
|
||||
Assert(i != end(_buttons));
|
||||
setActive(i - begin(_buttons));
|
||||
}
|
||||
|
||||
rpl::producer<QString> TabsStrip::activated() const {
|
||||
return _activated.events();
|
||||
}
|
||||
|
||||
QRect TabsStrip::islandRect() const {
|
||||
return rect() - _st.margin;
|
||||
}
|
||||
|
||||
int TabsStrip::islandInteriorWidth() const {
|
||||
return islandRect().width() - 2 * _st.skip;
|
||||
}
|
||||
|
||||
QRect TabsStrip::highlightRect(int index) const {
|
||||
Expects(index >= 0 && index < int(_buttons.size()));
|
||||
|
||||
return _buttons[index].geometry - Margins(_st.activeSkip);
|
||||
}
|
||||
|
||||
QRectF TabsStrip::currentHighlightRect() const {
|
||||
const auto progress = _activeAnimation.value(1.);
|
||||
const auto from = QRectF(_activeFrom);
|
||||
const auto to = QRectF(_activeTo);
|
||||
return QRectF(
|
||||
anim::interpolateF(from.x(), to.x(), progress),
|
||||
anim::interpolateF(from.y(), to.y(), progress),
|
||||
anim::interpolateF(from.width(), to.width(), progress),
|
||||
anim::interpolateF(from.height(), to.height(), progress));
|
||||
}
|
||||
|
||||
int TabsStrip::scrollValue() const {
|
||||
return int(base::SafeRound(_scroll));
|
||||
}
|
||||
|
||||
void TabsStrip::setSelected(int index) {
|
||||
const auto was = (_selected >= 0);
|
||||
const auto now = (index >= 0);
|
||||
_selected = index;
|
||||
if (was != now) {
|
||||
setCursor(now ? style::cur_pointer : style::cur_default);
|
||||
}
|
||||
}
|
||||
|
||||
void TabsStrip::setActive(int index) {
|
||||
if (_active == index) {
|
||||
return;
|
||||
}
|
||||
const auto previous = std::exchange(_active, index);
|
||||
if (index < 0) {
|
||||
_activeAnimation.stop();
|
||||
_wasActive = -1;
|
||||
update();
|
||||
return;
|
||||
}
|
||||
const auto to = highlightRect(index);
|
||||
if (previous < 0 || previous >= int(_buttons.size())) {
|
||||
_activeAnimation.stop();
|
||||
_activeFrom = _activeTo = to;
|
||||
_wasActive = -1;
|
||||
} else {
|
||||
const auto current = currentHighlightRect().toRect();
|
||||
_activeAnimation.stop();
|
||||
_activeFrom = current;
|
||||
_activeTo = to;
|
||||
_wasActive = previous;
|
||||
_activeAnimation.start(
|
||||
[=] { update(); },
|
||||
0.,
|
||||
1.,
|
||||
_st.duration,
|
||||
anim::easeOutQuint);
|
||||
}
|
||||
scrollToTab(index);
|
||||
update();
|
||||
}
|
||||
|
||||
void TabsStrip::scrollToTab(int index) {
|
||||
const auto interior = islandInteriorWidth();
|
||||
const auto geometry = _buttons[index].geometry;
|
||||
if (interior <= 0 || _scrollMax <= 0 || geometry.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
const auto added = std::max(
|
||||
std::min(interior / 8, (interior - geometry.width()) / 2),
|
||||
0);
|
||||
const auto visibleFrom = scrollValue();
|
||||
const auto visibleTill = visibleFrom + interior;
|
||||
if ((visibleTill < geometry.x() + geometry.width() + added)
|
||||
|| (visibleFrom + added > geometry.x())) {
|
||||
_scrollTo = std::clamp(
|
||||
geometry.x() + (geometry.width() / 2) - (interior / 2),
|
||||
0,
|
||||
_scrollMax);
|
||||
_scrollAnimation.start([=] {
|
||||
_scroll = _scrollAnimation.value(_scrollTo);
|
||||
update();
|
||||
}, _scroll, _scrollTo, crl::time(150), anim::easeOutCirc);
|
||||
}
|
||||
}
|
||||
|
||||
int TabsStrip::resizeGetHeight(int newWidth) {
|
||||
const auto interior = newWidth
|
||||
- _st.margin.left()
|
||||
- _st.margin.right()
|
||||
- 2 * _st.skip;
|
||||
_scrollMax = std::max(_fullWidth - interior, 0);
|
||||
if (_scroll > _scrollMax) {
|
||||
_scroll = _scrollMax;
|
||||
_scrollAnimation.stop();
|
||||
}
|
||||
return _buttons.empty() ? 0 : _st.height;
|
||||
}
|
||||
|
||||
bool TabsStrip::eventHook(QEvent *e) {
|
||||
if (e->type() == QEvent::Leave) {
|
||||
setSelected(-1);
|
||||
}
|
||||
return RpWidget::eventHook(e);
|
||||
}
|
||||
|
||||
void TabsStrip::mouseMoveEvent(QMouseEvent *e) {
|
||||
const auto mousex = e->pos().x();
|
||||
const auto drag = QApplication::startDragDistance();
|
||||
if (_dragx > 0) {
|
||||
_scrollAnimation.stop();
|
||||
_scroll = std::clamp(
|
||||
_dragscroll + _dragx - mousex,
|
||||
0.,
|
||||
_scrollMax * 1.);
|
||||
update();
|
||||
return;
|
||||
} else if (_pressx > 0 && std::abs(_pressx - mousex) > drag) {
|
||||
_dragx = _pressx;
|
||||
_dragscroll = _scroll;
|
||||
stopPressedRipple();
|
||||
}
|
||||
setSelected(indexAt(e->pos()));
|
||||
}
|
||||
|
||||
int TabsStrip::indexAt(QPoint position) const {
|
||||
const auto shifted = position - contentOrigin();
|
||||
for (auto i = 0, c = int(_buttons.size()); i != c; ++i) {
|
||||
if (_buttons[i].geometry.contains(shifted)) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
QPoint TabsStrip::contentOrigin() const {
|
||||
const auto island = islandRect();
|
||||
return QPoint(island.x() + _st.skip - scrollValue(), island.y());
|
||||
}
|
||||
|
||||
void TabsStrip::addRipple(int index, QPoint position) {
|
||||
auto &button = _buttons[index];
|
||||
if (!button.ripple) {
|
||||
const auto size = highlightRect(index).size();
|
||||
button.ripple = std::make_unique<Ui::RippleAnimation>(
|
||||
_st.ripple,
|
||||
Ui::RippleAnimation::RoundRectMask(size, size.height() / 2),
|
||||
[=] { update(); });
|
||||
}
|
||||
const auto highlight = highlightRect(index).translated(
|
||||
contentOrigin());
|
||||
button.ripple->add(position - highlight.topLeft());
|
||||
}
|
||||
|
||||
void TabsStrip::stopPressedRipple() {
|
||||
if (_pressed >= 0
|
||||
&& _pressed < int(_buttons.size())
|
||||
&& _buttons[_pressed].ripple) {
|
||||
_buttons[_pressed].ripple->lastStop();
|
||||
}
|
||||
}
|
||||
|
||||
void TabsStrip::wheelEvent(QWheelEvent *e) {
|
||||
const auto delta = Ui::ScrollDeltaF(e);
|
||||
|
||||
const auto phase = e->phase();
|
||||
const auto horizontal = (std::abs(delta.x()) > std::abs(delta.y()));
|
||||
if (phase == Qt::NoScrollPhase) {
|
||||
_locked = std::nullopt;
|
||||
} else if (phase == Qt::ScrollBegin) {
|
||||
_locked = std::nullopt;
|
||||
} else if (!_locked) {
|
||||
_locked = horizontal ? Qt::Horizontal : Qt::Vertical;
|
||||
}
|
||||
if (horizontal) {
|
||||
if (_locked == Qt::Vertical) {
|
||||
return;
|
||||
}
|
||||
e->accept();
|
||||
} else {
|
||||
if (_locked == Qt::Horizontal) {
|
||||
e->accept();
|
||||
} else {
|
||||
e->ignore();
|
||||
}
|
||||
return;
|
||||
}
|
||||
_scrollAnimation.stop();
|
||||
_scroll = std::clamp(_scroll - delta.x(), 0., _scrollMax * 1.);
|
||||
update();
|
||||
}
|
||||
|
||||
void TabsStrip::mousePressEvent(QMouseEvent *e) {
|
||||
if (e->button() != Qt::LeftButton) {
|
||||
return;
|
||||
}
|
||||
setSelected(indexAt(e->pos()));
|
||||
_pressed = _selected;
|
||||
_pressx = e->pos().x();
|
||||
if (_pressed >= 0) {
|
||||
addRipple(_pressed, e->pos());
|
||||
}
|
||||
}
|
||||
|
||||
void TabsStrip::mouseReleaseEvent(QMouseEvent *e) {
|
||||
if (e->button() != Qt::LeftButton) {
|
||||
return;
|
||||
}
|
||||
stopPressedRipple();
|
||||
const auto dragx = std::exchange(_dragx, 0);
|
||||
const auto pressed = std::exchange(_pressed, -1);
|
||||
_pressx = 0;
|
||||
if (!dragx
|
||||
&& pressed >= 0
|
||||
&& _selected == pressed
|
||||
&& pressed < _buttons.size()) {
|
||||
_activated.fire_copy(_buttons[pressed].tab.id);
|
||||
}
|
||||
}
|
||||
|
||||
void TabsStrip::paintEvent(QPaintEvent *e) {
|
||||
if (_buttons.empty()) {
|
||||
return;
|
||||
}
|
||||
auto p = QPainter(this);
|
||||
const auto island = islandRect();
|
||||
const auto radius = island.height() / 2.;
|
||||
_shadow.paint(p, island, int(radius));
|
||||
|
||||
auto hq = PainterHighQualityEnabler(p);
|
||||
p.setBrush(_st.bg);
|
||||
p.setPen(Qt::NoPen);
|
||||
p.drawRoundedRect(island, radius, radius);
|
||||
PaintIslandOutline(p, QRectF(island), radius, _st.bg);
|
||||
|
||||
auto clip = QPainterPath();
|
||||
clip.addRoundedRect(QRectF(island), radius, radius);
|
||||
p.setClipPath(clip);
|
||||
|
||||
const auto origin = contentOrigin();
|
||||
const auto progress = _activeAnimation.value(1.);
|
||||
const auto animating = _activeAnimation.animating();
|
||||
if (_active >= 0) {
|
||||
const auto highlight = currentHighlightRect().translated(
|
||||
origin.x(),
|
||||
origin.y());
|
||||
const auto highlightRadius = highlight.height() / 2.;
|
||||
p.setBrush(_st.bgActive);
|
||||
p.setPen(Qt::NoPen);
|
||||
p.drawRoundedRect(highlight, highlightRadius, highlightRadius);
|
||||
}
|
||||
for (auto i = 0, c = int(_buttons.size()); i != c; ++i) {
|
||||
auto &button = _buttons[i];
|
||||
if (!button.ripple) {
|
||||
continue;
|
||||
}
|
||||
const auto highlight = highlightRect(i).translated(origin);
|
||||
button.ripple->paint(p, highlight.x(), highlight.y(), width());
|
||||
if (button.ripple->empty()) {
|
||||
button.ripple.reset();
|
||||
}
|
||||
}
|
||||
const auto textTop = (_buttons.front().geometry.height()
|
||||
- _st.style.font->height) / 2;
|
||||
for (auto i = 0, c = int(_buttons.size()); i != c; ++i) {
|
||||
const auto &button = _buttons[i];
|
||||
if (i == _active) {
|
||||
p.setPen(animating
|
||||
? QPen(anim::color(_st.fg, _st.fgActive, progress))
|
||||
: QPen(_st.fgActive->c));
|
||||
} else if (animating && i == _wasActive) {
|
||||
p.setPen(anim::color(_st.fgActive, _st.fg, progress));
|
||||
} else {
|
||||
p.setPen(_st.fg->c);
|
||||
}
|
||||
button.text.draw(p, {
|
||||
.position = origin
|
||||
+ button.geometry.topLeft()
|
||||
+ QPoint(_st.tabPadding.left(), textTop),
|
||||
.availableWidth = button.text.maxWidth(),
|
||||
});
|
||||
}
|
||||
|
||||
if (_scrollMax > 0) {
|
||||
const auto &icon = st::defaultEmojiSuggestions;
|
||||
const auto &c = _st.bg->c;
|
||||
constexpr auto kF = 0.5;
|
||||
const auto rightWidth = icon.fadeRight.width();
|
||||
const auto opacityRight = (_scrollMax - _scroll)
|
||||
/ (rightWidth * kF);
|
||||
p.setOpacity(std::clamp(opacityRight, 0., 1.));
|
||||
icon.fadeRight.fill(
|
||||
p,
|
||||
QRect(
|
||||
island.x() + island.width() - rightWidth,
|
||||
island.y(),
|
||||
rightWidth,
|
||||
island.height()),
|
||||
c);
|
||||
|
||||
const auto leftWidth = icon.fadeLeft.width();
|
||||
const auto opacityLeft = _scroll / (leftWidth * kF);
|
||||
p.setOpacity(std::clamp(opacityLeft, 0., 1.));
|
||||
icon.fadeLeft.fill(
|
||||
p,
|
||||
QRect(island.x(), island.y(), leftWidth, island.height()),
|
||||
c);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace Info::Profile
|
||||
@@ -0,0 +1,91 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "ui/rp_widget.h"
|
||||
#include "ui/effects/animations.h"
|
||||
#include "ui/text/text.h"
|
||||
#include "ui/widgets/shadow.h"
|
||||
|
||||
namespace style {
|
||||
struct ProfileTabsStrip;
|
||||
} // namespace style
|
||||
|
||||
namespace Ui {
|
||||
class RippleAnimation;
|
||||
} // namespace Ui
|
||||
|
||||
namespace Info::Profile {
|
||||
|
||||
struct StripTab {
|
||||
QString id;
|
||||
QString text;
|
||||
};
|
||||
|
||||
class TabsStrip final : public Ui::RpWidget {
|
||||
public:
|
||||
TabsStrip(QWidget *parent, const style::ProfileTabsStrip &st);
|
||||
|
||||
void setTabs(std::vector<StripTab> tabs);
|
||||
void setActiveTab(const QString &id);
|
||||
|
||||
[[nodiscard]] rpl::producer<QString> activated() const;
|
||||
|
||||
private:
|
||||
struct Button {
|
||||
StripTab tab;
|
||||
QRect geometry;
|
||||
Ui::Text::String text;
|
||||
std::unique_ptr<Ui::RippleAnimation> ripple;
|
||||
};
|
||||
|
||||
int resizeGetHeight(int newWidth) override;
|
||||
void wheelEvent(QWheelEvent *e) override;
|
||||
void mouseMoveEvent(QMouseEvent *e) override;
|
||||
void mousePressEvent(QMouseEvent *e) override;
|
||||
void mouseReleaseEvent(QMouseEvent *e) override;
|
||||
void paintEvent(QPaintEvent *e) override;
|
||||
bool eventHook(QEvent *e) override;
|
||||
|
||||
void setSelected(int index);
|
||||
void setActive(int index);
|
||||
void scrollToTab(int index);
|
||||
void addRipple(int index, QPoint position);
|
||||
void stopPressedRipple();
|
||||
[[nodiscard]] int indexAt(QPoint position) const;
|
||||
[[nodiscard]] QPoint contentOrigin() const;
|
||||
[[nodiscard]] QRect islandRect() const;
|
||||
[[nodiscard]] int islandInteriorWidth() const;
|
||||
[[nodiscard]] QRect highlightRect(int index) const;
|
||||
[[nodiscard]] QRectF currentHighlightRect() const;
|
||||
[[nodiscard]] int scrollValue() const;
|
||||
|
||||
const style::ProfileTabsStrip &_st;
|
||||
Ui::BoxShadow _shadow;
|
||||
std::vector<Button> _buttons;
|
||||
rpl::event_stream<QString> _activated;
|
||||
std::optional<Qt::Orientation> _locked;
|
||||
int _dragx = 0;
|
||||
int _pressx = 0;
|
||||
float64 _dragscroll = 0.;
|
||||
float64 _scroll = 0.;
|
||||
float64 _scrollTo = 0.;
|
||||
Ui::Animations::Simple _scrollAnimation;
|
||||
Ui::Animations::Simple _activeAnimation;
|
||||
QRect _activeFrom;
|
||||
QRect _activeTo;
|
||||
int _scrollMax = 0;
|
||||
int _fullWidth = 0;
|
||||
int _selected = -1;
|
||||
int _pressed = -1;
|
||||
int _active = -1;
|
||||
int _wasActive = -1;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Info::Profile
|
||||
Reference in New Issue
Block a user