init
Some checks failed
Docker. / Ubuntu (push) Has been cancelled
User-agent updater. / User-agent (push) Failing after 15s
Lock Threads / lock (push) Failing after 10s
Waiting for answer. / waiting-for-answer (push) Failing after 22s
Needs user action. / needs-user-action (push) Failing after 8s
Can't reproduce. / cant-reproduce (push) Failing after 8s
Close stale issues and PRs / stale (push) Has been cancelled

This commit is contained in:
allhaileris
2026-02-16 15:50:16 +03:00
commit afb81b8278
13816 changed files with 3689732 additions and 0 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,915 @@
/*
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 "media/view/media_view_group_thumbs.h"
#include "data/data_shared_media.h"
#include "data/data_user_photos.h"
#include "data/data_photo.h"
#include "data/data_photo_media.h"
#include "data/data_document.h"
#include "data/data_document_media.h"
#include "data/data_media_types.h"
#include "data/data_session.h"
#include "data/data_web_page.h"
#include "data/data_file_origin.h"
#include "history/history.h"
#include "history/view/media/history_view_media.h"
#include "ui/image/image.h"
#include "ui/ui_utility.h"
#include "main/main_session.h"
#include "core/crash_reports.h"
#include "styles/style_media_view.h"
namespace Media {
namespace View {
namespace {
constexpr auto kThumbDuration = crl::time(150);
int Round(float64 value) {
return int(base::SafeRound(value));
}
using Context = GroupThumbs::Context;
using Key = GroupThumbs::Key;
#if 0
[[nodiscard]] QString DebugSerializeMsgId(FullMsgId itemId) {
return QString("msg%1_%2").arg(itemId.channel.bare).arg(itemId.msg);
}
[[nodiscard]] QString DebugSerializePeer(PeerId peerId) {
return peerIsUser(peerId)
? QString("user%1").arg(peerToUser(peerId).bare)
: peerIsChat(peerId)
? QString("chat%1").arg(peerToChat(peerId).bare)
: QString("channel%1").arg(peerToChannel(peerId).bare);
}
[[nodiscard]] QString DebugSerializeKey(const Key &key) {
return v::match(key, [&](PhotoId photoId) {
return QString("photo%1").arg(photoId);
}, [](FullMsgId itemId) {
return DebugSerializeMsgId(itemId);
}, [&](GroupThumbs::CollageKey key) {
return QString("collage%1").arg(key.index);
});
}
[[nodiscard]] QString DebugSerializeContext(const Context &context) {
return v::match(context, [](PeerId peerId) {
return DebugSerializePeer(peerId);
}, [](MessageGroupId groupId) {
return QString("group_%1_%2"
).arg(DebugSerializePeer(groupId.peer)
).arg(groupId.value);
}, [](FullMsgId item) {
return DebugSerializeMsgId(item);
}, [](v::null_t) -> QString {
return "null";
});
}
#endif
Data::FileOrigin ComputeFileOrigin(const Key &key, const Context &context) {
return v::match(key, [&](PhotoId photoId) {
return v::match(context, [&](PeerId peerId) {
return peerIsUser(peerId)
? Data::FileOriginUserPhoto(peerToUser(peerId), photoId)
: Data::FileOrigin(Data::FileOriginPeerPhoto(peerId));
}, [](auto&&) {
return Data::FileOrigin();
});
}, [](FullMsgId itemId) {
return Data::FileOrigin(itemId);
}, [&](GroupThumbs::CollageKey) {
return v::match(context, [](const GroupThumbs::CollageSlice &slice) {
return Data::FileOrigin(slice.context);
}, [](auto&&) {
return Data::FileOrigin();
});
});
}
Context ComputeContext(
not_null<Main::Session*> session,
const SharedMediaWithLastSlice &slice,
int index) {
Expects(index >= 0 && index < slice.size());
const auto value = slice[index];
if (const auto photo = std::get_if<not_null<PhotoData*>>(&value)) {
if (const auto peer = (*photo)->peer) {
return peer->id;
}
return v::null;
} else if (const auto msgId = std::get_if<FullMsgId>(&value)) {
if (const auto item = session->data().message(*msgId)) {
if (item->isService()) {
return item->history()->peer->id;
} else if (const auto groupId = item->groupId()) {
return groupId;
}
}
return v::null;
}
Unexpected("Variant in ComputeContext(SharedMediaWithLastSlice::Value)");
}
Context ComputeContext(
not_null<Main::Session*> session,
const UserPhotosSlice &slice,
int index) {
return peerFromUser(slice.key().userId);
}
Context ComputeContext(
not_null<Main::Session*> session,
const GroupThumbs::CollageSlice &slice,
int index) {
return slice.context;
}
Key ComputeKey(const SharedMediaWithLastSlice &slice, int index) {
Expects(index >= 0 && index < slice.size());
const auto value = slice[index];
if (const auto photo = std::get_if<not_null<PhotoData*>>(&value)) {
return (*photo)->id;
} else if (const auto msgId = std::get_if<FullMsgId>(&value)) {
return *msgId;
}
Unexpected("Variant in ComputeKey(SharedMediaWithLastSlice::Value)");
}
Key ComputeKey(const UserPhotosSlice &slice, int index) {
return slice[index];
}
Key ComputeKey(const GroupThumbs::CollageSlice &slice, int index) {
return GroupThumbs::CollageKey{ index };
}
int ComputeThumbsLimit(int availableWidth) {
const auto singleWidth = st::mediaviewGroupWidth
+ 2 * st::mediaviewGroupSkip;
const auto currentWidth = st::mediaviewGroupWidthMax
+ 2 * st::mediaviewGroupSkipCurrent;
const auto skipForAnimation = 2 * singleWidth;
const auto leftWidth = availableWidth
- currentWidth
- skipForAnimation;
return std::max(leftWidth / (2 * singleWidth), 1);
}
} // namespace
class GroupThumbs::Thumb {
public:
enum class State {
Unknown,
Current,
Alive,
Dying,
};
Thumb(Key key, Fn<void()> handler);
Thumb(
Key key,
not_null<PhotoData*> photo,
Data::FileOrigin origin,
Fn<void()> handler);
Thumb(
Key key,
not_null<DocumentData*> document,
Data::FileOrigin origin,
Fn<void()> handler);
[[nodiscard]] int leftToUpdate() const;
[[nodiscard]] int rightToUpdate() const;
void animateToLeft(not_null<Thumb*> next);
void animateToRight(not_null<Thumb*> prev);
void setState(State state);
[[nodiscard]] State state() const;
[[nodiscard]] bool inited() const;
[[nodiscard]] bool removed() const;
void paint(QPainter &p, int x, int y, int outerWidth, float64 progress);
[[nodiscard]] ClickHandlerPtr getState(QPoint point) const;
private:
QSize wantedPixSize() const;
void validateImage();
int currentLeft() const;
int currentWidth() const;
int finalLeft() const;
int finalWidth() const;
void animateTo(int left, int width);
ClickHandlerPtr _link;
const Key _key;
std::shared_ptr<Data::DocumentMedia> _documentMedia;
std::shared_ptr<Data::PhotoMedia> _photoMedia;
Image *_image = nullptr;
Data::FileOrigin _origin;
State _state = State::Alive;
QPixmap _full;
int _fullWidth = 0;
bool _hiding = false;
anim::value _left = { 0. };
anim::value _width = { 0. };
anim::value _opacity = { 0., 1. };
};
GroupThumbs::Thumb::Thumb(Key key, Fn<void()> handler)
: _key(key) {
_link = std::make_shared<LambdaClickHandler>(std::move(handler));
validateImage();
}
GroupThumbs::Thumb::Thumb(
Key key,
not_null<PhotoData*> photo,
Data::FileOrigin origin,
Fn<void()> handler)
: _key(key)
, _photoMedia(photo->createMediaView())
, _origin(origin) {
_link = std::make_shared<LambdaClickHandler>(std::move(handler));
_photoMedia->wanted(Data::PhotoSize::Thumbnail, origin);
validateImage();
}
GroupThumbs::Thumb::Thumb(
Key key,
not_null<DocumentData*> document,
Data::FileOrigin origin,
Fn<void()> handler)
: _key(key)
, _documentMedia(document->createMediaView())
, _origin(origin) {
_link = std::make_shared<LambdaClickHandler>(std::move(handler));
_documentMedia->thumbnailWanted(origin);
validateImage();
}
QSize GroupThumbs::Thumb::wantedPixSize() const {
const auto originalWidth = _image ? std::max(_image->width(), 1) : 1;
const auto originalHeight = _image ? std::max(_image->height(), 1) : 1;
const auto pixHeight = st::mediaviewGroupHeight;
const auto pixWidth = originalWidth * pixHeight / originalHeight;
return { pixWidth, pixHeight };
}
void GroupThumbs::Thumb::validateImage() {
if (!_image) {
if (_photoMedia) {
_image = _photoMedia->image(Data::PhotoSize::Thumbnail);
} else if (_documentMedia) {
_image = _documentMedia->thumbnail();
}
}
if (!_full.isNull() || !_image) {
return;
}
const auto pixSize = wantedPixSize();
if (pixSize.width() > st::mediaviewGroupWidthMax) {
const auto originalWidth = _image->width();
const auto originalHeight = _image->height();
const auto takeWidth = originalWidth * st::mediaviewGroupWidthMax
/ pixSize.width();
auto original = _image->original();
original.setDevicePixelRatio(style::DevicePixelRatio());
_full = Ui::PixmapFromImage(original.copy(
(originalWidth - takeWidth) / 2,
0,
takeWidth,
originalHeight
).scaled(
st::mediaviewGroupWidthMax * style::DevicePixelRatio(),
pixSize.height() * style::DevicePixelRatio(),
Qt::IgnoreAspectRatio,
Qt::SmoothTransformation));
} else {
_full = _image->pixNoCache(pixSize * style::DevicePixelRatio());
}
_fullWidth = std::min(
wantedPixSize().width(),
st::mediaviewGroupWidthMax);
}
int GroupThumbs::Thumb::leftToUpdate() const {
return Round(std::min(_left.from(), _left.to()));
}
int GroupThumbs::Thumb::rightToUpdate() const {
return Round(std::max(
_left.from() + _width.from(),
_left.to() + _width.to()));
}
int GroupThumbs::Thumb::currentLeft() const {
return Round(_left.current());
}
int GroupThumbs::Thumb::currentWidth() const {
return Round(_width.current());
}
int GroupThumbs::Thumb::finalLeft() const {
return Round(_left.to());
}
int GroupThumbs::Thumb::finalWidth() const {
return Round(_width.to());
}
void GroupThumbs::Thumb::setState(State state) {
const auto isNewThumb = (_state == State::Alive);
_state = state;
if (_state == State::Current) {
if (isNewThumb) {
_opacity = anim::value(1.);
_left = anim::value(-_fullWidth / 2);
_width = anim::value(_fullWidth);
} else {
_opacity.start(1.);
}
_hiding = false;
animateTo(-_fullWidth / 2, _fullWidth);
} else if (_state == State::Alive) {
_opacity.start(0.7);
_hiding = false;
} else if (_state == State::Dying) {
_opacity.start(0.);
_hiding = true;
_left.restart();
_width.restart();
}
}
void GroupThumbs::Thumb::animateTo(int left, int width) {
_left.start(left);
_width.start(width);
}
void GroupThumbs::Thumb::animateToLeft(not_null<Thumb*> next) {
const auto width = st::mediaviewGroupWidth;
if (_state == State::Alive) {
// New item animation, start exactly from the next, move only.
_left = anim::value(next->currentLeft() - width);
_width = anim::value(width);
} else if (_state == State::Unknown) {
// Existing item animation.
setState(State::Alive);
}
const auto skip1 = st::mediaviewGroupSkip;
const auto skip2 = (next->state() == State::Current)
? st::mediaviewGroupSkipCurrent
: st::mediaviewGroupSkip;
animateTo(next->finalLeft() - width - skip1 - skip2, width);
}
void GroupThumbs::Thumb::animateToRight(not_null<Thumb*> prev) {
const auto width = st::mediaviewGroupWidth;
if (_state == State::Alive) {
// New item animation, start exactly from the next, move only.
_left = anim::value(prev->currentLeft() + prev->currentWidth());
_width = anim::value(width);
} else if (_state == State::Unknown) {
// Existing item animation.
setState(State::Alive);
}
const auto skip1 = st::mediaviewGroupSkip;
const auto skip2 = (prev->state() == State::Current)
? st::mediaviewGroupSkipCurrent
: st::mediaviewGroupSkip;
animateTo(prev->finalLeft() + prev->finalWidth() + skip1 + skip2, width);
}
auto GroupThumbs::Thumb::state() const -> State {
return _state;
}
bool GroupThumbs::Thumb::inited() const {
return _fullWidth != 0;
}
bool GroupThumbs::Thumb::removed() const {
return (_state == State::Dying) && _hiding && !_opacity.current();
}
void GroupThumbs::Thumb::paint(
QPainter &p,
int x,
int y,
int outerWidth,
float64 progress) {
validateImage();
_opacity.update(progress, anim::linear);
_left.update(progress, anim::linear);
_width.update(progress, anim::linear);
const auto left = x + currentLeft();
const auto width = currentWidth();
const auto opacity = p.opacity();
p.setOpacity(_opacity.current() * opacity);
if (width == _fullWidth) {
p.drawPixmap(left, y, _full);
} else {
const auto takeWidth = width * style::DevicePixelRatio();
const auto from = QRect(
(_full.width() - takeWidth) / 2,
0,
takeWidth,
_full.height());
const auto to = QRect(left, y, width, st::mediaviewGroupHeight);
p.drawPixmap(to, _full, from);
}
p.setOpacity(opacity);
}
ClickHandlerPtr GroupThumbs::Thumb::getState(QPoint point) const {
if (_state != State::Alive) {
return nullptr;
}
const auto left = finalLeft();
const auto width = finalWidth();
return QRect(left, 0, width, st::mediaviewGroupHeight).contains(point)
? _link
: nullptr;
}
int GroupThumbs::CollageSlice::size() const {
return data->items.size();
}
GroupThumbs::GroupThumbs(not_null<Main::Session*> session, Context context)
: _session(session)
, _context(context) {
}
void GroupThumbs::updateContext(Context context) {
if (_context != context) {
clear();
_context = context;
}
}
template <typename Slice>
void GroupThumbs::RefreshFromSlice(
not_null<Main::Session*> session,
std::unique_ptr<GroupThumbs> &instance,
const Slice &slice,
int index,
int availableWidth) {
const auto context = ComputeContext(session, slice, index);
if (instance) {
instance->updateContext(context);
}
if (v::is_null(context)) {
if (instance) {
instance->resizeToWidth(availableWidth);
}
return;
}
const auto limit = ComputeThumbsLimit(availableWidth);
const auto from = [&] {
const auto edge = std::max(index - limit, 0);
for (auto result = index; result != edge; --result) {
if (ComputeContext(session, slice, result - 1) != context) {
return result;
}
}
return edge;
}();
const auto till = [&] {
const auto edge = std::min(index + limit + 1, slice.size());
for (auto result = index + 1; result != edge; ++result) {
if (ComputeContext(session, slice, result) != context) {
return result;
}
}
return edge;
}();
if (from + 1 < till) {
if (!instance) {
instance = std::make_unique<GroupThumbs>(session, context);
}
instance->fillItems(slice, from, index, till);
instance->resizeToWidth(availableWidth);
} else if (instance) {
instance->clear();
instance->resizeToWidth(availableWidth);
}
}
#if 0
template <typename Slice>
void ValidateSlice(
const Slice &slice,
const Context &context,
int from,
int index,
int till) {
auto keys = base::flat_set<Key>();
for (auto i = from; i != till; ++i) {
const auto key = ComputeKey(slice, i);
if (keys.contains(key)) {
// All items should be unique!
auto strings = QStringList();
strings.reserve(till - from);
for (auto i = from; i != till; ++i) {
strings.push_back(DebugSerializeKey(ComputeKey(slice, i)));
}
CrashReports::SetAnnotation(
"keys",
QString("%1:%2-(%3)-%4:"
).arg(DebugSerializeContext(context)
).arg(from
).arg(index
).arg(till) + strings.join(","));
if (Logs::DebugEnabled()) {
Unexpected("Bad slice in GroupThumbs.");
}
break;
} else {
keys.emplace(key);
}
}
}
#endif
template <typename Slice>
void GroupThumbs::fillItems(
const Slice &slice,
int from,
int index,
int till) {
Expects(from <= index);
Expects(index < till);
Expects(from + 1 < till);
const auto current = (index - from);
const auto old = base::take(_items);
//ValidateSlice(slice, _context, from, index, till);
markCacheStale();
_items.reserve(till - from);
for (auto i = from; i != till; ++i) {
_items.push_back(validateCacheEntry(ComputeKey(slice, i)));
}
animateAliveItems(current);
fillDyingItems(old);
startDelayedAnimation();
}
void GroupThumbs::animateAliveItems(int current) {
Expects(current >= 0 && current < _items.size());
_items[current]->setState(Thumb::State::Current);
for (auto i = current; i != 0;) {
const auto prev = _items[i];
const auto item = _items[--i];
item->animateToLeft(prev);
}
for (auto i = current + 1; i != _items.size(); ++i) {
const auto prev = _items[i - 1];
const auto item = _items[i];
item->animateToRight(prev);
}
}
void GroupThumbs::fillDyingItems(const std::vector<not_null<Thumb*>> &old) {
//Expects(_cache.size() >= _items.size());
if (_cache.size() >= _items.size()) {
_dying.reserve(_cache.size() - _items.size());
}
animatePreviouslyAlive(old);
markRestAsDying();
}
void GroupThumbs::markRestAsDying() {
//Expects(_cache.size() >= _items.size());
if (_cache.size() >= _items.size()) {
_dying.reserve(_cache.size() - _items.size());
}
for (const auto &cacheItem : _cache) {
const auto &thumb = cacheItem.second;
const auto state = thumb->state();
if (state == Thumb::State::Unknown) {
markAsDying(thumb.get());
}
}
}
void GroupThumbs::markAsDying(not_null<Thumb*> thumb) {
thumb->setState(Thumb::State::Dying);
_dying.push_back(thumb.get());
}
void GroupThumbs::animatePreviouslyAlive(
const std::vector<not_null<Thumb*>> &old) {
auto toRight = false;
for (auto i = 0; i != old.size(); ++i) {
const auto item = old[i];
if (item->state() == Thumb::State::Unknown) {
if (toRight) {
markAsDying(item);
item->animateToRight(old[i - 1]);
}
} else if (!toRight) {
for (auto j = i; j != 0;) {
const auto next = old[j];
const auto prev = old[--j];
markAsDying(prev);
prev->animateToLeft(next);
}
toRight = true;
}
}
}
auto GroupThumbs::createThumb(Key key)
-> std::unique_ptr<Thumb> {
if (const auto photoId = std::get_if<PhotoId>(&key)) {
const auto photo = _session->data().photo(*photoId);
return createThumb(key, photo);
} else if (const auto msgId = std::get_if<FullMsgId>(&key)) {
if (const auto item = _session->data().message(*msgId)) {
if (const auto media = item->media()) {
if (const auto photo = media->photo()) {
return createThumb(key, photo);
} else if (const auto document = media->document()) {
return createThumb(key, document);
}
}
}
return createThumb(key, nullptr);
} else if (const auto collageKey = std::get_if<CollageKey>(&key)) {
if (const auto itemId = std::get_if<FullMsgId>(&_context)) {
if (const auto item = _session->data().message(*itemId)) {
if (const auto media = item->media()) {
if (const auto page = media->webpage()) {
return createThumb(
key,
page->collage,
collageKey->index);
} else if (const auto invoice = media->invoice()) {
return createThumb(
key,
*invoice,
collageKey->index);
}
}
}
}
return createThumb(key, nullptr);
}
Unexpected("Value of Key in GroupThumbs::createThumb()");
}
auto GroupThumbs::createThumb(
Key key,
const WebPageCollage &collage,
int index)
-> std::unique_ptr<Thumb> {
if (index < 0 || index >= collage.items.size()) {
return createThumb(key, nullptr);
}
const auto &item = collage.items[index];
if (const auto photo = std::get_if<PhotoData*>(&item)) {
return createThumb(key, (*photo));
} else if (const auto document = std::get_if<DocumentData*>(&item)) {
return createThumb(key, (*document));
}
return createThumb(key, nullptr);
}
auto GroupThumbs::createThumb(
Key key,
const Data::Invoice &invoice,
int index)
-> std::unique_ptr<Thumb> {
if (index < 0 || index >= invoice.extendedMedia.size()) {
return createThumb(key, nullptr);
}
const auto &media = invoice.extendedMedia[index];
if (const auto photo = media->photo()) {
return createThumb(key, photo);
} else if (const auto document = media->document()) {
return createThumb(key, document);
}
return createThumb(key, nullptr);
}
auto GroupThumbs::createThumb(Key key, std::nullptr_t)
-> std::unique_ptr<Thumb> {
const auto weak = base::make_weak(this);
const auto origin = ComputeFileOrigin(key, _context);
return std::make_unique<Thumb>(key, [=] {
if (const auto strong = weak.get()) {
strong->_activateStream.fire_copy(key);
}
});
}
auto GroupThumbs::createThumb(Key key, not_null<PhotoData*> photo)
-> std::unique_ptr<Thumb> {
const auto weak = base::make_weak(this);
const auto origin = ComputeFileOrigin(key, _context);
return std::make_unique<Thumb>(key, photo, origin, [=] {
if (const auto strong = weak.get()) {
strong->_activateStream.fire_copy(key);
}
});
}
auto GroupThumbs::createThumb(Key key, not_null<DocumentData*> document)
-> std::unique_ptr<Thumb> {
const auto weak = base::make_weak(this);
const auto origin = ComputeFileOrigin(key, _context);
return std::make_unique<Thumb>(key, document, origin, [=] {
if (const auto strong = weak.get()) {
strong->_activateStream.fire_copy(key);
}
});
}
auto GroupThumbs::validateCacheEntry(Key key) -> not_null<Thumb*> {
const auto i = _cache.find(key);
return (i != _cache.end())
? i->second.get()
: _cache.emplace(key, createThumb(key)).first->second.get();
}
void GroupThumbs::markCacheStale() {
_dying.clear();
for (const auto &cacheItem : _cache) {
const auto &thumb = cacheItem.second;
thumb->setState(Thumb::State::Unknown);
}
}
void GroupThumbs::Refresh(
not_null<Main::Session*> session,
std::unique_ptr<GroupThumbs> &instance,
const SharedMediaWithLastSlice &slice,
int index,
int availableWidth) {
RefreshFromSlice(session, instance, slice, index, availableWidth);
}
void GroupThumbs::Refresh(
not_null<Main::Session*> session,
std::unique_ptr<GroupThumbs> &instance,
const UserPhotosSlice &slice,
int index,
int availableWidth) {
RefreshFromSlice(session, instance, slice, index, availableWidth);
}
void GroupThumbs::Refresh(
not_null<Main::Session*> session,
std::unique_ptr<GroupThumbs> &instance,
const CollageSlice &slice,
int index,
int availableWidth) {
RefreshFromSlice(session, instance, slice, index, availableWidth);
}
void GroupThumbs::clear() {
if (_items.empty()) {
return;
}
base::take(_items);
markCacheStale();
markRestAsDying();
startDelayedAnimation();
}
void GroupThumbs::startDelayedAnimation() {
_animation.stop();
_waitingForAnimationStart = true;
countUpdatedRect();
}
void GroupThumbs::resizeToWidth(int newWidth) {
_width = newWidth;
}
int GroupThumbs::height() const {
return st::mediaviewGroupPadding.top()
+ st::mediaviewGroupHeight
+ st::mediaviewGroupPadding.bottom();
}
bool GroupThumbs::hiding() const {
return _items.empty();
}
bool GroupThumbs::hidden() const {
return hiding() && !_waitingForAnimationStart && !_animation.animating();
}
void GroupThumbs::checkForAnimationStart() {
if (_waitingForAnimationStart) {
_waitingForAnimationStart = false;
_animation.start([=] { update(); }, 0., 1., kThumbDuration);
}
}
void GroupThumbs::update() {
if (_cache.empty()) {
return;
}
_updateRequests.fire_copy(_updatedRect);
}
void GroupThumbs::paint(QPainter &p, int x, int y, int outerWidth) {
const auto progress = _waitingForAnimationStart
? 0.
: _animation.value(1.);
x += (_width / 2);
y += st::mediaviewGroupPadding.top();
auto initedCurrentIndex = int(_items.size());
for (auto i = _cache.begin(); i != _cache.end();) {
const auto thumb = not_null{ i->second.get() };
const auto inited = thumb->inited();
thumb->paint(p, x, y, outerWidth, progress);
if (thumb->removed()) {
_dying.erase(ranges::remove(_dying, thumb), _dying.end());
i = _cache.erase(i);
} else {
if (!inited
&& thumb->inited()
&& thumb->state() == Thumb::State::Current) {
initedCurrentIndex = ranges::find(_items, thumb)
- begin(_items);
}
++i;
}
}
if (initedCurrentIndex < _items.size()) {
animateAliveItems(initedCurrentIndex);
}
}
ClickHandlerPtr GroupThumbs::getState(QPoint point) const {
point -= QPoint((_width / 2), st::mediaviewGroupPadding.top());
for (const auto &cacheItem : _cache) {
const auto &thumb = cacheItem.second;
if (auto link = thumb->getState(point)) {
return link;
}
}
return nullptr;
}
void GroupThumbs::countUpdatedRect() {
if (_cache.empty()) {
return;
}
auto min = _width;
auto max = 0;
const auto left = [](const auto &cacheItem) {
const auto &[key, thumb] = cacheItem;
return thumb->leftToUpdate();
};
const auto right = [](const auto &cacheItem) {
const auto &[key, thumb] = cacheItem;
return thumb->rightToUpdate();
};
accumulate_min(min, left(*ranges::max_element(
_cache,
std::greater<>(),
left)));
accumulate_max(max, right(*ranges::max_element(
_cache,
std::less<>(),
right)));
_updatedRect = QRect(
min,
st::mediaviewGroupPadding.top(),
max - min,
st::mediaviewGroupHeight);
}
GroupThumbs::~GroupThumbs() = default;
} // namespace View
} // namespace Media

View File

@@ -0,0 +1,152 @@
/*
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 "history/history_item_components.h"
#include "ui/effects/animations.h"
#include "base/weak_ptr.h"
class SharedMediaWithLastSlice;
class UserPhotosSlice;
struct WebPageCollage;
namespace Data {
struct Invoice;
} // namespace Data
namespace Main {
class Session;
} // namespace Main
namespace Media {
namespace View {
class GroupThumbs : public base::has_weak_ptr {
public:
struct CollageKey {
int index = 0;
inline bool operator<(const CollageKey &other) const {
return index < other.index;
}
};
struct CollageSlice {
FullMsgId context;
not_null<const WebPageCollage*> data;
int size() const;
};
using Key = std::variant<PhotoId, FullMsgId, CollageKey>;
static void Refresh(
not_null<Main::Session*> session,
std::unique_ptr<GroupThumbs> &instance,
const SharedMediaWithLastSlice &slice,
int index,
int availableWidth);
static void Refresh(
not_null<Main::Session*> session,
std::unique_ptr<GroupThumbs> &instance,
const UserPhotosSlice &slice,
int index,
int availableWidth);
static void Refresh(
not_null<Main::Session*> session,
std::unique_ptr<GroupThumbs> &instance,
const CollageSlice &slice,
int index,
int availableWidth);
void clear();
void resizeToWidth(int newWidth);
int height() const;
bool hiding() const;
bool hidden() const;
void checkForAnimationStart();
void paint(QPainter &p, int x, int y, int outerWidth);
ClickHandlerPtr getState(QPoint point) const;
rpl::producer<QRect> updateRequests() const {
return _updateRequests.events();
}
rpl::producer<Key> activateRequests() const {
return _activateStream.events();
}
rpl::lifetime &lifetime() {
return _lifetime;
}
using Context = std::variant<
v::null_t,
PeerId,
MessageGroupId,
FullMsgId>;
GroupThumbs(not_null<Main::Session*> session, Context context);
~GroupThumbs();
private:
class Thumb;
template <typename Slice>
static void RefreshFromSlice(
not_null<Main::Session*> session,
std::unique_ptr<GroupThumbs> &instance,
const Slice &slice,
int index,
int availableWidth);
template <typename Slice>
void fillItems(const Slice &slice, int from, int index, int till);
void updateContext(Context context);
void markCacheStale();
not_null<Thumb*> validateCacheEntry(Key key);
std::unique_ptr<Thumb> createThumb(Key key);
std::unique_ptr<Thumb> createThumb(
Key key,
const WebPageCollage &collage,
int index);
std::unique_ptr<Thumb> createThumb(
Key key,
const Data::Invoice &invoice,
int index);
std::unique_ptr<Thumb> createThumb(Key key, not_null<PhotoData*> photo);
std::unique_ptr<Thumb> createThumb(
Key key,
not_null<DocumentData*> document);
std::unique_ptr<Thumb> createThumb(Key key, std::nullptr_t);
void update();
void countUpdatedRect();
void animateAliveItems(int current);
void fillDyingItems(const std::vector<not_null<Thumb*>> &old);
void markAsDying(not_null<Thumb*> thumb);
void markRestAsDying();
void animatePreviouslyAlive(const std::vector<not_null<Thumb*>> &old);
void startDelayedAnimation();
const not_null<Main::Session*> _session;
Context _context;
bool _waitingForAnimationStart = true;
Ui::Animations::Simple _animation;
std::vector<not_null<Thumb*>> _items;
std::vector<not_null<Thumb*>> _dying;
base::flat_map<Key, std::unique_ptr<Thumb>> _cache;
int _width = 0;
QRect _updatedRect;
rpl::event_stream<QRect> _updateRequests;
rpl::event_stream<Key> _activateStream;
rpl::lifetime _lifetime;
};
} // namespace View
} // namespace Media

View File

@@ -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
*/
#include "media/view/media_view_open_common.h"
#include "history/history_item.h"
#include "data/data_media_types.h"
#include "data/data_web_page.h"
namespace Media::View {
TimeId ExtractVideoTimestamp(not_null<HistoryItem*> item) {
const auto media = item->media();
if (!media) {
return 0;
} else if (const auto timestamp = media->videoTimestamp()) {
return timestamp;
} else if (const auto webpage = media->webpage()) {
return webpage->extractVideoTimestamp();
}
return 0;
}
} // namespace Media::View

View File

@@ -0,0 +1,173 @@
/*
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 "data/data_cloud_themes.h"
#include "data/data_stories.h"
class DocumentData;
class PeerData;
class PhotoData;
class HistoryItem;
namespace Window {
class SessionController;
} // namespace Window
namespace Media::View {
struct OpenRequest {
public:
OpenRequest() {
}
OpenRequest(
Window::SessionController *controller,
not_null<PhotoData*> photo,
HistoryItem *item,
MsgId topicRootId,
PeerId monoforumPeerId)
: _controller(controller)
, _photo(photo)
, _item(item)
, _topicRootId(topicRootId)
, _monoforumPeerId(monoforumPeerId) {
}
OpenRequest(
Window::SessionController *controller,
not_null<PhotoData*> photo,
not_null<PeerData*> peer)
: _controller(controller)
, _photo(photo)
, _peer(peer) {
}
OpenRequest(
Window::SessionController *controller,
not_null<DocumentData*> document,
HistoryItem *item,
MsgId topicRootId,
PeerId monoforumPeerId,
bool continueStreaming = false,
crl::time startTime = 0)
: _controller(controller)
, _document(document)
, _item(item)
, _topicRootId(topicRootId)
, _monoforumPeerId(monoforumPeerId)
, _continueStreaming(continueStreaming)
, _startTime(startTime) {
}
OpenRequest(
Window::SessionController *controller,
not_null<DocumentData*> document,
const Data::CloudTheme &cloudTheme)
: _controller(controller)
, _document(document)
, _cloudTheme(cloudTheme) {
}
OpenRequest(
Window::SessionController *controller,
not_null<Data::Story*> story,
Data::StoriesContext context)
: _controller(controller)
, _story(story)
, _storiesContext(context) {
}
OpenRequest(
Window::SessionController *controller,
std::shared_ptr<Data::GroupCall> call,
QString linkSlug,
MsgId joinMessageId)
: _controller(controller)
, _call(std::move(call))
, _callLinkSlug(std::move(linkSlug))
, _callJoinMessageId(joinMessageId) {
}
[[nodiscard]] PeerData *peer() const {
return _peer;
}
[[nodiscard]] PhotoData *photo() const {
return _photo;
}
[[nodiscard]] HistoryItem *item() const {
return _item;
}
[[nodiscard]] MsgId topicRootId() const {
return _topicRootId;
}
[[nodiscard]] PeerId monoforumPeerId() const {
return _monoforumPeerId;
}
[[nodiscard]] DocumentData *document() const {
return _document;
}
[[nodiscard]] Data::Story *story() const {
return _story;
}
[[nodiscard]] Data::StoriesContext storiesContext() const {
return _storiesContext;
}
[[nodiscard]] const std::shared_ptr<Data::GroupCall> &call() const {
return _call;
}
[[nodiscard]] const QString &callLinkSlug() const {
return _callLinkSlug;
}
[[nodiscard]] MsgId callJoinMessageId() const {
return _callJoinMessageId;
}
[[nodiscard]] std::optional<Data::CloudTheme> cloudTheme() const {
return _cloudTheme;
}
[[nodiscard]] Window::SessionController *controller() const {
return _controller;
}
[[nodiscard]] bool continueStreaming() const {
return _continueStreaming;
}
[[nodiscard]] crl::time startTime() const {
return _startTime;
}
private:
Window::SessionController *_controller = nullptr;
DocumentData *_document = nullptr;
PhotoData *_photo = nullptr;
Data::Story *_story = nullptr;
Data::StoriesContext _storiesContext;
PeerData *_peer = nullptr;
HistoryItem *_item = nullptr;
MsgId _topicRootId = 0;
PeerId _monoforumPeerId = 0;
std::optional<Data::CloudTheme> _cloudTheme = std::nullopt;
bool _continueStreaming = false;
crl::time _startTime = 0;
std::shared_ptr<Data::GroupCall> _call;
QString _callLinkSlug;
MsgId _callJoinMessageId = 0;
};
[[nodiscard]] TimeId ExtractVideoTimestamp(not_null<HistoryItem*> item);
} // namespace Media::View

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,169 @@
/*
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 "media/view/media_view_overlay_renderer.h"
#include "ui/gl/gl_image.h"
#include "ui/gl/gl_primitives.h"
#include <QOpenGLBuffer>
namespace Media::View {
class OverlayWidget::RendererGL final
: public OverlayWidget::Renderer
, public base::has_weak_ptr {
public:
explicit RendererGL(not_null<OverlayWidget*> owner);
void init(QOpenGLFunctions &f) override;
void deinit(QOpenGLFunctions *f) override;
void paint(
not_null<QOpenGLWidget*> widget,
QOpenGLFunctions &f) override;
std::optional<QColor> clearColor() override;
private:
struct Control {
int index = -1;
not_null<const style::icon*> icon;
};
bool handleHideWorkaround(QOpenGLFunctions &f);
void paintBackground() override;
void paintVideoStream() override;
void paintTransformedVideoFrame(ContentGeometry geometry) override;
void paintTransformedStaticContent(
const QImage &image,
ContentGeometry geometry,
bool semiTransparent,
bool fillTransparentBackground,
int index = 0) override;
void paintTransformedContent(
not_null<QOpenGLShaderProgram*> program,
ContentGeometry geometry,
bool fillTransparentBackground);
void paintRadialLoading(
QRect inner,
bool radial,
float64 radialOpacity) override;
void paintThemePreview(QRect outer) override;
void paintDocumentBubble(QRect outer, QRect icon) override;
void paintSaveMsg(QRect outer) override;
void paintControlsStart() override;
void paintControl(
Over control,
QRect over,
float64 overOpacity,
QRect inner,
float64 innerOpacity,
const style::icon &icon) override;
void paintFooter(QRect outer, float64 opacity) override;
void paintCaption(QRect outer, float64 opacity) override;
void paintGroupThumbs(QRect outer, float64 opacity) override;
void paintRoundedCorners(int radius) override;
void paintStoriesSiblingPart(
int index,
const QImage &image,
QRect rect,
float64 opacity = 1.) override;
void paintRecognitionOverlay(
const QImage &image,
ContentGeometry geometry,
float64 opacity);
//void invalidate();
void paintUsingRaster(
Ui::GL::Image &image,
QRect rect,
Fn<void(Painter&&)> method,
int bufferOffset,
bool transparent = false);
void validateControlsFade();
void validateControls();
void invalidateControls();
void toggleBlending(bool enabled);
[[nodiscard]] Ui::GL::Rect transformRect(const QRect &raster) const;
[[nodiscard]] Ui::GL::Rect transformRect(const QRectF &raster) const;
[[nodiscard]] Ui::GL::Rect transformRect(
const Ui::GL::Rect &raster) const;
[[nodiscard]] Ui::GL::Rect scaleRect(
const Ui::GL::Rect &unscaled,
float64 scale) const;
void uploadTexture(
GLint internalformat,
GLint format,
QSize size,
QSize hasSize,
int stride,
const void *data) const;
const not_null<OverlayWidget*> _owner;
QOpenGLFunctions *_f = nullptr;
QSize _viewport;
float _factor = 1.;
int _ifactor = 1;
QVector2D _uniformViewport;
std::optional<QOpenGLBuffer> _contentBuffer;
std::optional<QOpenGLShaderProgram> _imageProgram;
std::optional<QOpenGLShaderProgram> _staticContentProgram;
QOpenGLShader *_texturedVertexShader = nullptr;
std::optional<QOpenGLShaderProgram> _withTransparencyProgram;
std::optional<QOpenGLShaderProgram> _yuv420Program;
std::optional<QOpenGLShaderProgram> _nv12Program;
std::optional<QOpenGLShaderProgram> _fillProgram;
std::optional<QOpenGLShaderProgram> _controlsProgram;
std::optional<QOpenGLShaderProgram> _roundedCornersProgram;
Ui::GL::Textures<6> _textures; // image, sibling, right sibling, y, u, v
QSize _rgbaSize[3];
QSize _lumaSize;
QSize _chromaSize;
qint64 _cacheKeys[3] = { 0 }; // image, sibling, right sibling
int _trackFrameIndex = 0;
int _streamedIndex = 0;
bool _chromaNV12 = false;
Ui::GL::Image _controlsFadeImage;
Ui::GL::Image _radialImage;
Ui::GL::Image _documentBubbleImage;
Ui::GL::Image _themePreviewImage;
Ui::GL::Image _saveMsgImage;
Ui::GL::Image _footerImage;
Ui::GL::Image _captionImage;
Ui::GL::Image _groupThumbsImage;
Ui::GL::Image _controlsImage;
static constexpr auto kStoriesSiblingPartsCount = 4;
Ui::GL::Image _storiesSiblingParts[kStoriesSiblingPartsCount];
static constexpr auto kControlsCount = 7;
[[nodiscard]] Control controlMeta(Over control) const;
// Last one is for the over circle image.
std::array<QRect, kControlsCount + 1> _controlsTextures;
bool _shadowTopFlip = false;
bool _shadowsForStories = false;
bool _blendingEnabled = false;
rpl::lifetime _storiesLifetime;
rpl::lifetime _lifetime;
};
} // namespace Media::View

View File

@@ -0,0 +1,322 @@
/*
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 "media/view/media_view_overlay_raster.h"
#include "ui/painter.h"
#include "media/stories/media_stories_view.h"
#include "media/view/media_view_pip.h"
#include "media/view/media_view_video_stream.h"
#include "platform/platform_overlay_widget.h"
#include "styles/style_media_view.h"
namespace Media::View {
OverlayWidget::RendererSW::RendererSW(not_null<OverlayWidget*> owner)
: _owner(owner)
, _transparentBrush(style::TransparentPlaceholder()) {
}
bool OverlayWidget::RendererSW::handleHideWorkaround() {
// This is needed on Windows or Linux,
// because on reopen it blinks with the last shown content.
return _owner->_hideWorkaround != nullptr;
}
void OverlayWidget::RendererSW::paintFallback(
Painter &p,
const QRegion &clip,
Ui::GL::Backend backend) {
if (handleHideWorkaround()) {
p.setCompositionMode(QPainter::CompositionMode_Source);
p.fillRect(clip.boundingRect(), Qt::transparent);
return;
}
if (const auto stream = _owner->_videoStream.get()) {
stream->ensureBorrowedRenderer();
}
_p = &p;
_clip = &clip;
_clipOuter = clip.boundingRect();
_owner->paint(this);
_p = nullptr;
_clip = nullptr;
}
void OverlayWidget::RendererSW::paintBackground() {
const auto region = _owner->opaqueContentShown()
? (*_clip - _owner->finalContentRect())
: *_clip;
const auto m = _p->compositionMode();
_p->setCompositionMode(QPainter::CompositionMode_Source);
const auto &bg = _owner->_fullScreenVideo
? st::mediaviewVideoBg
: st::mediaviewBg;
for (const auto &rect : region) {
_p->fillRect(rect, bg);
}
if (const auto notch = _owner->topNotchSkip()) {
const auto top = QRect(0, 0, _owner->width(), notch);
if (const auto black = top.intersected(_clipOuter); !black.isEmpty()) {
_p->fillRect(black, Qt::black);
}
}
_p->setCompositionMode(m);
}
void OverlayWidget::RendererSW::paintVideoStream() {
_owner->_videoStream->borrowedPaint(*_p, *_clip);
}
QRect OverlayWidget::RendererSW::TransformRect(
QRectF geometry,
int rotation) {
const auto center = geometry.center();
const auto rect = ((rotation % 180) == 90)
? QRectF(
center.x() - geometry.height() / 2.,
center.y() - geometry.width() / 2.,
geometry.height(),
geometry.width())
: geometry;
return QRect(
int(rect.x()),
int(rect.y()),
int(rect.width()),
int(rect.height()));
}
void OverlayWidget::RendererSW::paintTransformedVideoFrame(
ContentGeometry geometry) {
Expects(_owner->_streamed != nullptr);
const auto rotation = int(geometry.rotation);
const auto rect = TransformRect(geometry.rect, rotation);
if (!rect.intersects(_clipOuter)) {
return;
}
paintTransformedImage(_owner->videoFrame(), rect, rotation);
paintControlsFade(rect, geometry);
}
void OverlayWidget::RendererSW::paintTransformedStaticContent(
const QImage &image,
ContentGeometry geometry,
bool semiTransparent,
bool fillTransparentBackground,
int index) {
const auto rotation = int(geometry.rotation);
const auto rect = TransformRect(geometry.rect, rotation);
if (!rect.intersects(_clipOuter)) {
return;
}
if (fillTransparentBackground) {
_p->fillRect(rect, _transparentBrush);
}
if (!image.isNull()) {
paintTransformedImage(image, rect, rotation);
}
paintControlsFade(rect, geometry);
}
void OverlayWidget::RendererSW::paintControlsFade(
QRect content,
const ContentGeometry &geometry) {
auto opacity = geometry.controlsOpacity;
if (geometry.fade > 0.) {
_p->setOpacity(geometry.fade);
_p->fillRect(content, Qt::black);
opacity *= 1. - geometry.fade;
}
_p->setOpacity(opacity);
_p->setClipRect(content);
const auto width = _owner->width();
const auto stories = (_owner->_stories != nullptr);
if (!stories || geometry.topShadowShown) {
const auto flip = !stories && !_owner->topShadowOnTheRight();
const auto &top = stories
? st::storiesShadowTop
: st::mediaviewShadowTop;
const auto topShadow = stories
? QRect(
content.topLeft(),
QSize(content.width(), top.height()))
: QRect(
QPoint(flip ? 0 : (width - top.width()), 0),
top.size());
if (topShadow.intersected(content).intersects(_clipOuter)) {
if (stories) {
top.fill(*_p, topShadow);
} else if (flip) {
if (_topShadowCache.isNull()
|| _topShadowColor != st::windowShadowFg->c) {
_topShadowColor = st::windowShadowFg->c;
_topShadowCache = top.instance(
_topShadowColor).mirrored(true, false);
}
_p->drawImage(0, 0, _topShadowCache);
} else {
top.paint(*_p, topShadow.topLeft(), width);
}
}
}
const auto &bottom = stories
? st::storiesShadowBottom
: st::mediaviewShadowBottom;
const auto bottomStart = _owner->height() - geometry.bottomShadowSkip;
const auto bottomShadow = QRect(
QPoint(0, bottomStart - bottom.height()),
QSize(width, bottom.height()));
if (bottomShadow.intersected(content).intersects(_clipOuter)) {
bottom.fill(*_p, bottomShadow);
}
_p->setClipping(false);
_p->setOpacity(1.);
if (bottomStart < content.y() + content.height()) {
_p->fillRect(
content.x(),
bottomStart,
content.width(),
content.y() + content.height() - bottomStart,
QColor(0, 0, 0, 88));
}
}
void OverlayWidget::RendererSW::paintTransformedImage(
const QImage &image,
QRect rect,
int rotation) {
PainterHighQualityEnabler hq(*_p);
if (UsePainterRotation(rotation)) {
if (rotation) {
_p->save();
_p->rotate(rotation);
}
_p->drawImage(RotatedRect(rect, rotation), image);
if (rotation) {
_p->restore();
}
} else {
_p->drawImage(rect, _owner->transformShownContent(image, rotation));
}
}
void OverlayWidget::RendererSW::paintRadialLoading(
QRect inner,
bool radial,
float64 radialOpacity) {
_owner->paintRadialLoadingContent(*_p, inner, radial, radialOpacity);
}
void OverlayWidget::RendererSW::paintThemePreview(QRect outer) {
_owner->paintThemePreviewContent(*_p, outer, _clipOuter);
}
void OverlayWidget::RendererSW::paintDocumentBubble(
QRect outer,
QRect icon) {
if (outer.intersects(_clipOuter)) {
_owner->paintDocumentBubbleContent(*_p, outer, icon, _clipOuter);
if (icon.intersects(_clipOuter)) {
_owner->paintRadialLoading(this);
}
}
}
void OverlayWidget::RendererSW::paintSaveMsg(QRect outer) {
if (outer.intersects(_clipOuter)) {
_owner->paintSaveMsgContent(*_p, outer, _clipOuter);
}
}
void OverlayWidget::RendererSW::paintControlsStart() {
}
void OverlayWidget::RendererSW::paintControl(
Over control,
QRect over,
float64 overOpacity,
QRect inner,
float64 innerOpacity,
const style::icon &icon) {
if (!over.isEmpty() && !over.intersects(_clipOuter)) {
return;
}
if (!over.isEmpty() && overOpacity > 0) {
if (_overControlImage.isNull()) {
validateOverControlImage();
}
_p->setOpacity(overOpacity);
_p->drawImage(over.topLeft(), _overControlImage);
}
if (inner.intersects(_clipOuter)) {
_p->setOpacity(innerOpacity);
icon.paintInCenter(*_p, inner);
}
}
void OverlayWidget::RendererSW::paintFooter(QRect outer, float64 opacity) {
if (outer.intersects(_clipOuter)) {
_owner->paintFooterContent(*_p, outer, _clipOuter, opacity);
}
}
void OverlayWidget::RendererSW::paintCaption(QRect outer, float64 opacity) {
if (outer.intersects(_clipOuter)) {
_owner->paintCaptionContent(*_p, outer, _clipOuter, opacity);
}
}
void OverlayWidget::RendererSW::paintGroupThumbs(
QRect outer,
float64 opacity) {
if (outer.intersects(_clipOuter)) {
_owner->paintGroupThumbsContent(*_p, outer, _clipOuter, opacity);
}
}
void OverlayWidget::RendererSW::paintRoundedCorners(int radius) {
// The RpWindow rounding overlay will do the job.
}
void OverlayWidget::RendererSW::paintStoriesSiblingPart(
int index,
const QImage &image,
QRect rect,
float64 opacity) {
const auto changeOpacity = (opacity != 1.);
if (changeOpacity) {
_p->setOpacity(opacity);
}
_p->drawImage(rect, image);
if (changeOpacity) {
_p->setOpacity(1.);
}
}
void OverlayWidget::RendererSW::validateOverControlImage() {
const auto size = QSize(st::mediaviewIconOver, st::mediaviewIconOver);
const auto alpha = base::SafeRound(kOverBackgroundOpacity * 255);
_overControlImage = QImage(
size * style::DevicePixelRatio(),
QImage::Format_ARGB32_Premultiplied);
_overControlImage.setDevicePixelRatio(style::DevicePixelRatio());
_overControlImage.fill(Qt::transparent);
Painter p(&_overControlImage);
PainterHighQualityEnabler hq(p);
p.setPen(Qt::NoPen);
auto color = OverBackgroundColor();
color.setAlpha(alpha);
p.setBrush(color);
p.drawEllipse(QRect(QPoint(), size));
}
} // namespace Media::View

View File

@@ -0,0 +1,82 @@
/*
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 "media/view/media_view_overlay_renderer.h"
namespace Media::View {
class OverlayWidget::RendererSW final : public OverlayWidget::Renderer {
public:
explicit RendererSW(not_null<OverlayWidget*> owner);
void paintFallback(
Painter &p,
const QRegion &clip,
Ui::GL::Backend backend) override;
private:
void paintBackground() override;
void paintVideoStream() override;
void paintTransformedVideoFrame(ContentGeometry geometry) override;
void paintTransformedStaticContent(
const QImage &image,
ContentGeometry geometry,
bool semiTransparent,
bool fillTransparentBackground,
int index = 0) override;
void paintTransformedImage(
const QImage &image,
QRect rect,
int rotation);
void paintControlsFade(QRect content, const ContentGeometry &geometry);
void paintRadialLoading(
QRect inner,
bool radial,
float64 radialOpacity) override;
void paintThemePreview(QRect outer) override;
void paintDocumentBubble(QRect outer, QRect icon) override;
void paintSaveMsg(QRect outer) override;
void paintControlsStart() override;
void paintControl(
Over control,
QRect over,
float64 overOpacity,
QRect inner,
float64 innerOpacity,
const style::icon &icon) override;
void paintFooter(QRect outer, float64 opacity) override;
void paintCaption(QRect outer, float64 opacity) override;
void paintGroupThumbs(QRect outer, float64 opacity) override;
void paintRoundedCorners(int radius) override;
void paintStoriesSiblingPart(
int index,
const QImage &image,
QRect rect,
float64 opacity = 1.) override;
bool handleHideWorkaround();
void validateOverControlImage();
[[nodiscard]] static QRect TransformRect(QRectF geometry, int rotation);
const not_null<OverlayWidget*> _owner;
QBrush _transparentBrush;
Painter *_p = nullptr;
const QRegion *_clip = nullptr;
QRect _clipOuter;
QImage _overControlImage;
QImage _topShadowCache;
QColor _topShadowColor;
};
} // namespace Media::View

View File

@@ -0,0 +1,55 @@
/*
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 "media/view/media_view_overlay_widget.h"
namespace Media::Stories {
struct SiblingView;
} // namespace Media::Stories
namespace Media::View {
class OverlayWidget::Renderer : public Ui::GL::Renderer {
public:
virtual void paintBackground() = 0;
virtual void paintVideoStream() = 0;
virtual void paintTransformedVideoFrame(ContentGeometry geometry) = 0;
virtual void paintTransformedStaticContent(
const QImage &image,
ContentGeometry geometry,
bool semiTransparent,
bool fillTransparentBackground,
int index = 0) = 0; // image, left sibling, right sibling
virtual void paintRadialLoading(
QRect inner,
bool radial,
float64 radialOpacity) = 0;
virtual void paintThemePreview(QRect outer) = 0;
virtual void paintDocumentBubble(QRect outer, QRect icon) = 0;
virtual void paintSaveMsg(QRect outer) = 0;
virtual void paintControlsStart() = 0;
virtual void paintControl(
Over control,
QRect over,
float64 overOpacity,
QRect inner,
float64 innerOpacity,
const style::icon &icon) = 0;
virtual void paintFooter(QRect outer, float64 opacity) = 0;
virtual void paintCaption(QRect outer, float64 opacity) = 0;
virtual void paintGroupThumbs(QRect outer, float64 opacity) = 0;
virtual void paintRoundedCorners(int radius) = 0;
virtual void paintStoriesSiblingPart(
int index,
const QImage &image,
QRect rect,
float64 opacity = 1.) = 0;
};
} // namespace Media::View

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,782 @@
/*
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/timer.h"
#include "ui/rp_widget.h"
#include "ui/gl/gl_surface.h"
#include "ui/effects/animations.h"
#include "ui/effects/radial_animation.h"
#include "data/data_shared_media.h"
#include "data/data_user_photos.h"
#include "data/data_web_page.h"
#include "data/data_cloud_themes.h" // Data::CloudTheme.
#include "media/stories/media_stories_delegate.h"
#include "media/view/media_view_playback_controls.h"
#include "media/view/media_view_open_common.h"
#include "media/media_common.h"
#include "platform/platform_text_recognition.h"
class History;
namespace anim {
enum class activation : uchar;
} // namespace anim
namespace Calls {
class GroupCall;
} // namespace Calls
namespace Data {
class GroupCall;
class PhotoMedia;
class DocumentMedia;
struct StoriesContext;
} // namespace Data
namespace Ui {
class DropdownMenu;
class PopupMenu;
class LinkButton;
class RoundButton;
class RpWindow;
class LayerManager;
} // namespace Ui
namespace Ui::GL {
class Window;
struct ChosenRenderer;
enum class Backend;
} // namespace Ui::GL
namespace Ui::Menu {
struct MenuCallback;
} // namespace Ui::Menu
namespace Platform {
class OverlayWidgetHelper;
} // namespace Platform
namespace Window::Theme {
struct Preview;
} // namespace Window::Theme
namespace Media::Player {
struct TrackState;
} // namespace Media::Player
namespace Media::Streaming {
struct Information;
struct Update;
struct FrameWithInfo;
enum class Error;
} // namespace Media::Streaming
namespace Media::Stories {
class View;
struct ContentLayout;
} // namespace Media::Stories
namespace Media::View {
class VideoStream;
class PlaybackSponsored;
class GroupThumbs;
class Pip;
class OverlayWidget final
: public ClickHandlerHost
, private PlaybackControls::Delegate
, private Stories::Delegate {
public:
OverlayWidget();
~OverlayWidget();
enum class TouchBarItemType {
Photo,
Video,
None,
};
[[nodiscard]] bool isActive() const;
[[nodiscard]] bool isHidden() const;
[[nodiscard]] bool isMinimized() const;
[[nodiscard]] bool isFullScreen() const;
[[nodiscard]] not_null<QWidget*> widget() const;
void hide();
void setCursor(style::cursor cursor);
void setFocus();
[[nodiscard]] bool takeFocusFrom(not_null<QWidget*> window) const;
void activate();
void show(OpenRequest request);
void activateControls();
void close();
void minimize();
void toggleFullScreen();
void toggleFullScreen(bool fullscreen);
void notifyFileDialogShown(bool shown);
void clearSession();
// ClickHandlerHost interface
void clickHandlerActiveChanged(const ClickHandlerPtr &p, bool active) override;
void clickHandlerPressedChanged(const ClickHandlerPtr &p, bool pressed) override;
rpl::lifetime &lifetime();
private:
class Show;
struct Streamed;
struct PipWrap;
struct ItemContext;
struct StoriesContext;
class Renderer;
class RendererSW;
class RendererGL;
class SponsoredButton;
// If changing, see paintControls()!
enum class Over {
None,
Left,
Right,
LeftStories,
RightStories,
SponsoredButton,
Header,
Name,
Date,
Save,
Share,
Rotate,
More,
Recognize,
Icon,
Video,
Caption,
};
struct Entity {
std::variant<
v::null_t,
not_null<PhotoData*>,
not_null<DocumentData*>> data;
HistoryItem *item = nullptr;
MsgId topicRootId = 0;
PeerId monoforumPeerId = 0;
};
enum class SavePhotoVideo {
None,
QuickSave,
SaveAs,
};
struct ContentGeometry {
QRectF rect;
qreal rotation = 0.;
qreal controlsOpacity = 0.;
// Stories.
qreal fade = 0.;
qreal scale = 1.;
int bottomShadowSkip = 0;
int roundRadius = 0;
bool topShadowShown = false;
};
struct StartStreaming {
StartStreaming() : continueStreaming(false), startTime(0) {
}
StartStreaming(bool continueStreaming, crl::time startTime)
: continueStreaming(continueStreaming)
, startTime(startTime) {
}
const bool continueStreaming = false;
const crl::time startTime = 0;
};
[[nodiscard]] not_null<QWindow*> window() const;
[[nodiscard]] int width() const;
[[nodiscard]] int height() const;
void update();
void update(const QRegion &region);
[[nodiscard]] Ui::GL::ChosenRenderer chooseRenderer(
Ui::GL::Backend backend);
void paint(not_null<Renderer*> renderer);
void setupWindow();
void orderWidgets();
void showAndActivate();
void handleMousePress(QPoint position, Qt::MouseButton button);
void handleMouseRelease(QPoint position, Qt::MouseButton button);
void handleMouseMove(QPoint position);
bool handleContextMenu(std::optional<QPoint> position);
bool handleDoubleClick(QPoint position, Qt::MouseButton button);
bool handleTouchEvent(not_null<QTouchEvent*> e);
void handleWheelEvent(not_null<QWheelEvent*> e);
void handleKeyPress(not_null<QKeyEvent*> e);
void toggleApplicationEventFilter(bool install);
bool filterApplicationEvent(
not_null<QObject*> object,
not_null<QEvent*> e);
void setSession(not_null<Main::Session*> session);
void playbackControlsPlay() override;
void playbackControlsPause() override;
void playbackControlsSeekProgress(crl::time position) override;
void playbackControlsSeekFinished(crl::time position) override;
void playbackControlsVolumeChanged(float64 volume) override;
float64 playbackControlsCurrentVolume() override;
void playbackControlsVolumeToggled() override;
void playbackControlsVolumeChangeFinished() override;
void playbackControlsSpeedChanged(float64 speed) override;
float64 playbackControlsCurrentSpeed(bool lastNonDefault) override;
std::vector<int> playbackControlsQualities() override;
VideoQuality playbackControlsCurrentQuality() override;
void playbackControlsQualityChanged(int quality) override;
void playbackControlsToFullScreen() override;
void playbackControlsFromFullScreen() override;
void playbackControlsToPictureInPicture() override;
void playbackControlsRotate() override;
void playbackPauseResume();
void playbackToggleFullScreen();
void playbackPauseOnCall();
void playbackResumeOnCall();
void playbackPauseMusic();
void switchToPip();
[[nodiscard]] int topNotchSkip() const;
[[nodiscard]] std::shared_ptr<ChatHelpers::Show> uiShow();
not_null<Ui::RpWidget*> storiesWrap() override;
std::shared_ptr<ChatHelpers::Show> storiesShow() override;
auto storiesStickerOrEmojiChosen()
-> rpl::producer<ChatHelpers::FileChosen> override;
void storiesRedisplay(not_null<Data::Story*> story) override;
void storiesJumpTo(
not_null<Main::Session*> session,
FullStoryId id,
Data::StoriesContext context) override;
void storiesClose() override;
bool storiesPaused() override;
rpl::producer<bool> storiesLayerShown() override;
void storiesTogglePaused(bool paused) override;
float64 storiesSiblingOver(Stories::SiblingType type) override;
void storiesRepaint() override;
void storiesVolumeToggle() override;
void storiesVolumeChanged(float64 volume) override;
void storiesVolumeChangeFinished() override;
int storiesTopNotchSkip() override;
void hideControls(bool force = false);
void subscribeToScreenGeometry();
void toMessage();
void saveAs();
void downloadMedia();
void saveCancel();
void showInFolder();
void forwardMedia();
void deleteMedia();
void showMediaOverview();
void copyMedia();
void recognize();
void receiveMouse();
void showAttachedStickers();
[[nodiscard]] auto scaledRecognitionRect(QPoint position)
const -> std::optional<Platform::TextRecognition::RectWithText>;
void showDropdown();
void handleTouchTimer();
void handleDocumentClick();
[[nodiscard]] bool canShareAtTime() const;
[[nodiscard]] TimeId shareAtVideoTimestamp() const;
void shareAtTime();
void showSaveMsgToast(const QString &path, auto phrase);
void showSaveMsgToastWith(
const QString &path,
const TextWithEntities &text,
crl::time duration = 0);
void updateSaveMsg();
void clearBeforeHide();
void clearAfterHide();
void assignMediaPointer(DocumentData *document);
void assignMediaPointer(not_null<PhotoData*> photo);
void assignMediaPointer(std::shared_ptr<Data::GroupCall> call);
void updateOver(QPoint mpos);
void initFullScreen();
void initNormalGeometry();
void savePosition();
void moveToScreen(bool inMove = false);
void updateGeometry(bool inMove = false);
void updateGeometryToScreen(bool inMove = false);
bool moveToNext(int delta);
void preloadData(int delta);
void handleScreenChanged(not_null<QScreen*> screen);
[[nodiscard]] bool computeSaveButtonVisible() const;
void checkForSaveLoaded();
void showPremiumDownloadPromo();
[[nodiscard]] Entity entityForUserPhotos(int index) const;
[[nodiscard]] Entity entityForSharedMedia(int index) const;
[[nodiscard]] Entity entityForCollage(int index) const;
[[nodiscard]] Entity entityByIndex(int index) const;
[[nodiscard]] Entity entityForItemId(const FullMsgId &itemId) const;
bool moveToEntity(const Entity &entity, int preloadDelta = 0);
void setContext(std::variant<
v::null_t,
ItemContext,
not_null<PeerData*>,
StoriesContext> context);
void setStoriesPeer(PeerData *peer);
void refreshLang();
void showSaveMsgFile();
struct SharedMedia;
using SharedMediaType = SharedMediaWithLastSlice::Type;
using SharedMediaKey = SharedMediaWithLastSlice::Key;
[[nodiscard]] std::optional<SharedMediaType> sharedMediaType() const;
[[nodiscard]] std::optional<SharedMediaKey> sharedMediaKey() const;
[[nodiscard]] std::optional<SharedMediaType> computeOverviewType() const;
bool validSharedMedia() const;
void validateSharedMedia();
void handleSharedMediaUpdate(SharedMediaWithLastSlice &&update);
struct UserPhotos;
using UserPhotosKey = UserPhotosSlice::Key;
[[nodiscard]] std::optional<UserPhotosKey> userPhotosKey() const;
bool validUserPhotos() const;
void validateUserPhotos();
void handleUserPhotosUpdate(UserPhotosSlice &&update);
struct Collage;
using CollageKey = WebPageCollage::Item;
[[nodiscard]] std::optional<CollageKey> collageKey() const;
bool validCollage() const;
void validateCollage();
[[nodiscard]] Data::FileOrigin fileOrigin() const;
[[nodiscard]] Data::FileOrigin fileOrigin(const Entity& entity) const;
void refreshFromLabel();
void refreshCaption();
void refreshMediaViewer();
void refreshNavVisibility();
void refreshGroupThumbs();
void dropdownHidden();
void updateDocSize();
void updateControls();
void updateControlsGeometry();
void updateNavigationControlsGeometry();
void fillContextMenuActions(const Ui::Menu::MenuCallback &addAction);
void resizeCenteredControls();
void resizeContentByScreenSize();
void recountSkipTop();
void displayPhoto(
not_null<PhotoData*> photo,
anim::activation activation = anim::activation::normal);
void displayDocument(
DocumentData *document,
anim::activation activation = anim::activation::normal,
const Data::CloudTheme &cloud = Data::CloudTheme(),
const StartStreaming &startStreaming = StartStreaming());
void displayVideoStream(
const std::shared_ptr<Data::GroupCall> &call,
anim::activation activation = anim::activation::normal);
void displayFinished(anim::activation activation);
void redisplayContent();
void findCurrent();
void updateCursor();
void setZoomLevel(int newZoom, bool force = false);
void updatePlaybackState();
void seekRelativeTime(crl::time time);
void restartAtProgress(float64 progress);
void restartAtSeekPosition(crl::time position);
void refreshClipControllerGeometry();
void refreshCaptionGeometry();
bool initStreaming(
const StartStreaming &startStreaming = StartStreaming());
void startStreamingPlayer(const StartStreaming &startStreaming);
void initStreamingThumbnail();
void markStreamedReady();
void streamingReady(Streaming::Information &&info);
[[nodiscard]] bool createStreamingObjects();
void handleStreamingUpdate(Streaming::Update &&update);
void handleStreamingError(Streaming::Error &&error);
void updatePowerSaveBlocker(const Player::TrackState &state);
void initThemePreview();
void destroyThemePreview();
void updateThemePreviewGeometry();
void initSponsoredButton();
void refreshSponsoredButtonGeometry();
void refreshSponsoredButtonWidth();
void documentUpdated(not_null<DocumentData*> document);
void changingMsgId(FullMsgId newId, MsgId oldId);
[[nodiscard]] int finalContentRotation() const;
[[nodiscard]] QRect finalContentRect() const;
[[nodiscard]] ContentGeometry contentGeometry() const;
[[nodiscard]] ContentGeometry storiesContentGeometry(
const Stories::ContentLayout &layout,
float64 scale = 1.) const;
void updateContentRect();
void contentSizeChanged();
// Radial animation interface.
[[nodiscard]] float64 radialProgress() const;
[[nodiscard]] bool radialLoading() const;
[[nodiscard]] QRect radialRect() const;
void radialStart();
[[nodiscard]] crl::time radialTimeShift() const;
void updateHeader();
void snapXY();
void clearControlsState();
bool stateAnimationCallback(crl::time ms);
bool radialAnimationCallback(crl::time now);
void waitingAnimationCallback();
bool updateControlsAnimation(crl::time now);
void zoomIn();
void zoomOut();
void zoomReset();
void zoomUpdate(int32 &newZoom);
void paintRadialLoading(not_null<Renderer*> renderer);
void paintRadialLoadingContent(
Painter &p,
QRect inner,
bool radial,
float64 radialOpacity) const;
void paintThemePreviewContent(Painter &p, QRect outer, QRect clip);
void paintDocumentBubbleContent(
Painter &p,
QRect outer,
QRect icon,
QRect clip) const;
void paintSaveMsgContent(Painter &p, QRect outer, QRect clip);
void paintControls(not_null<Renderer*> renderer, float64 opacity);
void paintFooterContent(
Painter &p,
QRect outer,
QRect clip,
float64 opacity);
[[nodiscard]] QRect footerGeometry() const;
void paintCaptionContent(
Painter &p,
QRect outer,
QRect clip,
float64 opacity);
[[nodiscard]] QRect captionGeometry() const;
void paintGroupThumbsContent(
Painter &p,
QRect outer,
QRect clip,
float64 opacity);
[[nodiscard]] float64 controlOpacity(
float64 progress,
bool nonbright = false) const;
[[nodiscard]] bool isSaveMsgShown() const;
void updateOverRect(Over state);
bool updateOverState(Over newState);
float64 overLevel(Over control) const;
void checkGroupThumbsAnimation();
void initGroupThumbs();
void validatePhotoImage(Image *image, bool blurred);
void validatePhotoCurrentImage();
[[nodiscard]] bool hasCopyMediaRestriction(
bool skipPremiumCheck = false) const;
[[nodiscard]] bool showCopyMediaRestriction(
bool skipPRemiumCheck = false);
[[nodiscard]] QSize flipSizeByRotation(QSize size) const;
void applyVideoSize();
[[nodiscard]] bool videoShown() const;
[[nodiscard]] QSize videoSize() const;
[[nodiscard]] bool streamingRequiresControls() const;
[[nodiscard]] QImage videoFrame() const; // ARGB (changes prepare format)
[[nodiscard]] QImage currentVideoFrameImage() const; // RGB (may convert)
[[nodiscard]] Streaming::FrameWithInfo videoFrameWithInfo() const; // YUV
[[nodiscard]] int streamedIndex() const;
[[nodiscard]] QImage transformedShownContent() const;
[[nodiscard]] QImage transformShownContent(
QImage content,
int rotation) const;
[[nodiscard]] bool documentContentShown() const;
[[nodiscard]] bool documentBubbleShown() const;
void setStaticContent(QImage image);
[[nodiscard]] bool contentShown() const;
[[nodiscard]] bool opaqueContentShown() const;
void clearStreaming(bool savePosition = true);
[[nodiscard]] bool canInitStreaming() const;
[[nodiscard]] bool saveControlLocked() const;
void applyVideoQuality(VideoQuality value);
[[nodiscard]] bool topShadowOnTheRight() const;
void applyHideWindowWorkaround();
[[nodiscard]] ClickHandlerPtr ensureCaptionExpandLink();
Window::SessionController *findWindow(bool switchTo = true) const;
bool _opengl = false;
const std::unique_ptr<Ui::GL::Window> _wrap;
const not_null<Ui::RpWindow*> _window;
const std::unique_ptr<Platform::OverlayWidgetHelper> _helper;
const not_null<Ui::RpWidget*> _body;
const std::unique_ptr<Ui::RpWidget> _titleBugWorkaround;
const std::unique_ptr<Ui::RpWidgetWrap> _surface;
const not_null<QWidget*> _widget;
QRect _normalGeometry;
bool _wasWindowedMode = false;
bool _fullscreenInited = false;
bool _normalGeometryInited = false;
bool _fullscreen = true;
bool _windowed = false;
base::weak_ptr<Window::Controller> _openedFrom;
Main::Session *_session = nullptr;
rpl::lifetime _sessionLifetime;
PhotoData *_photo = nullptr;
DocumentData *_document = nullptr;
DocumentData *_chosenQuality = nullptr;
PhotoData *_videoCover = nullptr;
Media::VideoQuality _quality;
QString _documentLoadingTo;
std::shared_ptr<Data::PhotoMedia> _photoMedia;
std::shared_ptr<Data::DocumentMedia> _documentMedia;
std::shared_ptr<Data::PhotoMedia> _videoCoverMedia;
base::flat_set<std::shared_ptr<Data::PhotoMedia>> _preloadPhotos;
base::flat_set<std::shared_ptr<Data::DocumentMedia>> _preloadDocuments;
int _rotation = 0;
std::unique_ptr<SharedMedia> _sharedMedia;
std::optional<SharedMediaWithLastSlice> _sharedMediaData;
std::optional<SharedMediaWithLastSlice::Key> _sharedMediaDataKey;
std::unique_ptr<UserPhotos> _userPhotos;
std::optional<UserPhotosSlice> _userPhotosData;
std::unique_ptr<Collage> _collage;
std::optional<WebPageCollage> _collageData;
QRect _leftNav, _leftNavOver, _leftNavIcon;
QRect _rightNav, _rightNavOver, _rightNavIcon;
QRect _headerNav, _nameNav, _dateNav;
QRect _rotateNav, _rotateNavOver, _rotateNavIcon;
QRect _shareNav, _shareNavOver, _shareNavIcon;
QRect _recognizeNav, _recognizeNavOver, _recognizeNavIcon;
QRect _saveNav, _saveNavOver, _saveNavIcon;
QRect _moreNav, _moreNavOver, _moreNavIcon;
bool _leftNavVisible = false;
bool _rightNavVisible = false;
bool _saveVisible = false;
bool _shareVisible = false;
bool _rotateVisible = false;
bool _recognizeVisible = false;
bool _headerHasLink = false;
QString _dateText;
QString _headerText;
bool _streamingStartPaused = false;
bool _fullScreenVideo = false;
int _fullScreenZoomCache = 0;
float64 _lastPositiveVolume = 1.;
std::unique_ptr<GroupThumbs> _groupThumbs;
QRect _groupThumbsRect;
int _groupThumbsAvailableWidth = 0;
int _groupThumbsLeft = 0;
int _groupThumbsTop = 0;
Ui::Text::String _caption;
QRect _captionRect;
ClickHandlerPtr _captionExpandLink;
int _captionShowMoreWidth = 0;
int _captionSkipBlockWidth = 0;
int _topNotchSize = 0;
int _width = 0;
int _height = 0;
int _skipTop = 0;
int _availableHeight = 0;
int _minUsedTop = 0; // Geometry without top notch on macOS.
int _maxUsedHeight = 0;
int _x = 0, _y = 0, _w = 0, _h = 0;
int _xStart = 0, _yStart = 0;
int _zoom = 0; // < 0 - out, 0 - none, > 0 - in
float64 _zoomToScreen = 0.; // for documents
float64 _zoomToDefault = 0.;
QPoint _mStart;
bool _pressed = false;
bool _cursorOverriden = false;
int32 _dragging = 0;
QImage _staticContent;
bool _staticContentTransparent = false;
bool _blurred = true;
bool _reShow = false;
ContentGeometry _oldGeometry;
Ui::Animations::Simple _geometryAnimation;
rpl::lifetime _screenGeometryLifetime;
std::unique_ptr<QObject> _applicationEventFilter;
std::unique_ptr<Streamed> _streamed;
std::unique_ptr<PipWrap> _pip;
QImage _streamedQualityChangeFrame;
crl::time _streamedPosition = 0;
int _streamedCreated = 0;
bool _streamedQualityChangeFinished = false;
bool _showAsPip = false;
Qt::Orientations _flip;
std::unique_ptr<Stories::View> _stories;
std::shared_ptr<Show> _cachedShow;
rpl::event_stream<> _storiesChanged;
Main::Session *_storiesSession = nullptr;
rpl::event_stream<ChatHelpers::FileChosen> _storiesStickerOrEmojiChosen;
std::unique_ptr<Ui::LayerManager> _layerBg;
std::unique_ptr<VideoStream> _videoStream;
QString _callLinkSlug;
MsgId _callJoinMessageId;
const style::icon *_docIcon = nullptr;
style::color _docIconColor;
QString _docName, _docSize, _docExt;
int _docNameWidth = 0, _docSizeWidth = 0, _docExtWidth = 0;
QRect _docRect, _docIconRect;
QImage _docRectImage;
int _docThumbx = 0, _docThumby = 0, _docThumbw = 0;
object_ptr<Ui::LinkButton> _docDownload;
object_ptr<Ui::LinkButton> _docSaveAs;
object_ptr<Ui::LinkButton> _docCancel;
QRect _bottomShadowRect;
QRect _topShadowRect;
rpl::variable<bool> _topShadowRight = false;
QRect _photoRadialRect;
Ui::RadialAnimation _radial;
History *_migrated = nullptr;
History *_history = nullptr; // if conversation photos or files overview
MsgId _topicRootId = 0;
PeerId _monoforumPeerId = 0;
PeerData *_peer = nullptr;
UserData *_user = nullptr; // if user profile photos overview
// We save the information about the reason of the current mediaview show:
// did we open a peer profile photo or a photo from some message.
// We use it when trying to delete a photo: if we've opened a peer photo,
// then we'll delete group photo instead of the corresponding message.
bool _firstOpenedPeerPhoto = false;
PeerData *_from = nullptr;
QString _fromName;
Ui::Text::String _fromNameLabel;
std::optional<int> _index; // Index in current _sharedMedia data.
std::optional<int> _fullIndex; // Index in full shared media.
std::optional<int> _fullCount;
HistoryItem *_message = nullptr;
mtpRequestId _loadRequest = 0;
Over _over = Over::None;
Over _down = Over::None;
QPoint _lastAction, _lastMouseMovePos;
bool _ignoringDropdown = false;
Ui::Animations::Basic _stateAnimation;
enum ControlsState {
ControlsShowing,
ControlsShown,
ControlsHiding,
ControlsHidden,
};
ControlsState _controlsState = ControlsShown;
crl::time _controlsAnimStarted = 0;
base::Timer _controlsHideTimer;
anim::value _controlsOpacity = { 1. };
bool _mousePressed = false;
base::unique_qptr<Ui::PopupMenu> _menu;
object_ptr<Ui::DropdownMenu> _dropdown;
base::Timer _dropdownShowTimer;
base::unique_qptr<SponsoredButton> _sponsoredButton;
bool _receiveMouse = true;
bool _processingKeyPress = false;
bool _touchPress = false;
bool _touchMove = false;
bool _touchRightButton = false;
base::Timer _touchTimer;
QPoint _touchStart;
QString _saveMsgFilename;
QRect _saveMsg;
Ui::Text::String _saveMsgText;
SavePhotoVideo _savePhotoVideoWhenLoaded = SavePhotoVideo::None;
// _saveMsgAnimation -> _saveMsgTimer -> _saveMsgAnimation.
Ui::Animations::Simple _saveMsgAnimation;
base::Timer _saveMsgTimer;
base::flat_map<Over, crl::time> _animations;
base::flat_map<Over, anim::value> _animationOpacities;
rpl::event_stream<Media::Player::TrackState> _touchbarTrackState;
rpl::event_stream<TouchBarItemType> _touchbarDisplay;
rpl::event_stream<bool> _touchbarFullscreenToggled;
int _verticalWheelDelta = 0;
Platform::TextRecognition::Result _recognitionResult;
bool _showRecognitionResults = false;
Ui::Animations::Simple _recognitionAnimation;
bool _themePreviewShown = false;
uint64 _themePreviewId = 0;
QRect _themePreviewRect;
std::unique_ptr<Window::Theme::Preview> _themePreview;
object_ptr<Ui::RoundButton> _themeApply = { nullptr };
object_ptr<Ui::RoundButton> _themeCancel = { nullptr };
object_ptr<Ui::RoundButton> _themeShare = { nullptr };
Data::CloudTheme _themeCloudData;
std::unique_ptr<Ui::RpWidget> _hideWorkaround;
};
} // namespace Media::View

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,319 @@
/*
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 "data/data_file_origin.h"
#include "media/streaming/media_streaming_instance.h"
#include "media/media_common.h"
#include "ui/effects/animations.h"
#include "ui/round_rect.h"
#include "ui/rp_widget.h"
#include <QtCore/QPointer>
class HistoryItem;
namespace base {
class PowerSaveBlocker;
} // namespace base
namespace Data {
class DocumentMedia;
} // namespace Data
namespace style {
struct Shadow;
} // namespace style
namespace Ui {
class IconButton;
template <typename Widget>
class FadeWrap;
namespace GL {
struct ChosenRenderer;
struct Capabilities;
} // namespace GL
} // namespace Ui
namespace Media::Player {
struct TrackState;
} // namespace Media::Player
namespace Media::Streaming {
class Document;
} // namespace Media::Streaming
namespace Media::View {
class PlaybackProgress;
[[nodiscard]] QRect RotatedRect(QRect rect, int rotation);
[[nodiscard]] bool UsePainterRotation(int rotation);
[[nodiscard]] QSize FlipSizeByRotation(QSize size, int rotation);
[[nodiscard]] QImage RotateFrameImage(QImage image, int rotation);
[[nodiscard]] const style::Shadow &PipShadow();
class PipPanel final {
public:
struct Position {
RectParts attached = RectPart(0);
RectParts snapped = RectPart(0);
QRect geometry;
QRect screen;
};
PipPanel(
QWidget *parent,
Fn<Ui::GL::ChosenRenderer(Ui::GL::Capabilities)> renderer);
void init();
[[nodiscard]] not_null<QWidget*> widget() const;
[[nodiscard]] not_null<Ui::RpWidgetWrap*> rp() const;
void update();
void setGeometry(QRect geometry);
void setAspectRatio(QSize ratio);
[[nodiscard]] Position countPosition() const;
void setPosition(Position position);
[[nodiscard]] QRect inner() const;
[[nodiscard]] RectParts attached() const;
[[nodiscard]] bool useTransparency() const;
void setDragDisabled(bool disabled);
[[nodiscard]] bool dragging() const;
void handleWaylandResize(QSize size);
void handleScreenChanged(not_null<QScreen*> screen);
void handleMousePress(QPoint position, Qt::MouseButton button);
void handleMouseRelease(QPoint position, Qt::MouseButton button);
void handleMouseMove(QPoint position);
[[nodiscard]] rpl::producer<> saveGeometryRequests() const;
private:
void setPositionDefault();
void setPositionOnScreen(Position position, QRect available);
QScreen *myScreen() const;
void startSystemDrag();
void processDrag(QPoint point);
void finishDrag(QPoint point);
void updatePositionAnimated();
void updateOverState(QPoint point);
void moveAnimated(QPoint to);
void updateDecorations();
const std::unique_ptr<Ui::RpWidgetWrap> _content;
const QPointer<QWidget> _parent;
RectParts _attached = RectParts();
RectParts _snapped = RectParts();
QSize _ratio;
bool _useTransparency = true;
bool _dragDisabled = false;
bool _inHandleWaylandResize = false;
style::margins _padding;
RectPart _overState = RectPart();
std::optional<RectPart> _pressState;
QPoint _pressPoint;
QRect _dragStartGeometry;
std::optional<RectPart> _dragState;
rpl::event_stream<> _saveGeometryRequests;
QPoint _positionAnimationFrom;
QPoint _positionAnimationTo;
Ui::Animations::Simple _positionAnimation;
};
class Pip final {
public:
class Delegate {
public:
virtual void pipSaveGeometry(QByteArray geometry) = 0;
[[nodiscard]] virtual QByteArray pipLoadGeometry() = 0;
[[nodiscard]] virtual float64 pipPlaybackSpeed() = 0;
[[nodiscard]] virtual QWidget *pipParentWidget() = 0;
};
Pip(
not_null<Delegate*> delegate,
not_null<DocumentData*> data,
Data::FileOrigin origin,
not_null<DocumentData*> chosenQuality,
HistoryItem *context,
VideoQuality quality,
std::shared_ptr<Streaming::Document> shared,
FnMut<void()> closeAndContinue,
FnMut<void()> destroy);
~Pip();
[[nodiscard]] std::shared_ptr<Streaming::Document> shared() const;
private:
enum class OverState {
None,
Close,
Enlarge,
Playback,
VolumeToggle,
VolumeController,
Other,
};
enum class ThumbState {
Empty,
Inline,
Thumb,
Good,
Cover,
};
struct Button {
QRect area;
QRect icon;
OverState state = OverState::None;
Ui::Animations::Simple active;
};
struct ContentGeometry {
QRect inner;
RectParts attached = RectParts();
float64 fade = 0.;
QSize outer;
int rotation = 0;
int videoRotation = 0;
bool useTransparency = false;
};
struct StaticContent {
QImage image;
bool blurred = false;
};
using FrameRequest = Streaming::FrameRequest;
class Renderer;
class RendererGL;
class RendererSW;
void setupPanel();
void setupButtons();
void setupStreaming();
void playbackPauseResume();
void volumeChanged(float64 volume);
void volumeToggled();
void volumeControllerUpdate(QPoint position);
void waitingAnimationCallback();
void handleStreamingUpdate(Streaming::Update &&update);
void handleStreamingError(Streaming::Error &&error);
void saveGeometry();
void updatePlaybackState();
void updatePowerSaveBlocker(const Player::TrackState &state);
void updatePlayPauseResumeState(const Player::TrackState &state);
void restartAtSeekPosition(crl::time position);
[[nodiscard]] bool canUseVideoFrame() const;
[[nodiscard]] QImage videoFrame(const FrameRequest &request) const;
[[nodiscard]] Streaming::FrameWithInfo videoFrameWithInfo() const; // YUV
[[nodiscard]] QImage staticContent() const;
[[nodiscard]] OverState computeState(QPoint position) const;
void setOverState(OverState state);
void setPressedState(std::optional<OverState> state);
[[nodiscard]] OverState shownActiveState() const;
[[nodiscard]] float64 activeValue(const Button &button) const;
void updateActiveState(OverState was);
void updatePlaybackTexts(int64 position, int64 length, int64 frequency);
[[nodiscard]] static OverState ResolveShownOver(OverState state);
[[nodiscard]] Ui::GL::ChosenRenderer chooseRenderer(
Ui::GL::Capabilities capabilities);
void paint(not_null<Renderer*> renderer) const;
void handleMouseMove(QPoint position);
void handleMousePress(QPoint position, Qt::MouseButton button);
void handleMouseRelease(QPoint position, Qt::MouseButton button);
void handleDoubleClick(Qt::MouseButton button);
void handleLeave();
void handleClose();
void paintRadialLoadingContent(
QPainter &p,
const QRect &inner,
QColor fg) const;
void paintButtons(not_null<Renderer*> renderer, float64 shown) const;
void paintPlayback(not_null<Renderer*> renderer, float64 shown) const;
void paintPlaybackContent(QPainter &p, QRect outer, float64 shown) const;
void paintPlaybackProgress(QPainter &p, QRect outer) const;
void paintProgressBar(
QPainter &p,
const QRect &rect,
float64 progress,
int radius,
float64 active) const;
void paintPlaybackTexts(QPainter &p, QRect outer) const;
void paintVolumeController(
not_null<Renderer*> renderer,
float64 shown) const;
void paintVolumeControllerContent(
QPainter &p,
QRect outer,
float64 shown) const;
[[nodiscard]] QRect countRadialRect() const;
void applyVideoQuality(VideoQuality value);
[[nodiscard]] QImage currentVideoFrameImage() const;
void seekUpdate(QPoint position);
void seekProgress(float64 value);
void seekFinish(float64 value);
const not_null<Delegate*> _delegate;
const not_null<DocumentData*> _data;
const Data::FileOrigin _origin;
DocumentData *_chosenQuality = nullptr;
HistoryItem *_context = nullptr;
Media::VideoQuality _quality;
std::optional<Streaming::Instance> _instance;
bool _opengl = false;
PipPanel _panel;
QSize _size;
std::unique_ptr<base::PowerSaveBlocker> _powerSaveBlocker;
std::unique_ptr<PlaybackProgress> _playbackProgress;
std::shared_ptr<Data::DocumentMedia> _dataMedia;
QImage _qualityChangeFrame;
bool _qualityChangeFinished = false;
crl::time _lastUpdatePosition = 0;
bool _showPause = false;
bool _startPaused = false;
bool _pausedBySeek = false;
QString _timeAlready, _timeLeft;
int _timeLeftWidth = 0;
int _rotation = 0;
float64 _lastPositiveVolume = 1.;
crl::time _seekPositionMs = -1;
crl::time _lastDurationMs = 0;
OverState _over = OverState::None;
std::optional<OverState> _pressed;
std::optional<OverState> _lastHandledPress;
Button _close;
Button _enlarge;
Button _playback;
Button _play;
Button _volumeToggle;
Button _volumeController;
Ui::Animations::Simple _controlsShown;
FnMut<void()> _closeAndContinue;
FnMut<void()> _destroy;
mutable QImage _preparedCoverStorage;
mutable ThumbState _preparedCoverState = ThumbState::Empty;
};
} // namespace Media::View

View File

@@ -0,0 +1,812 @@
/*
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 "media/view/media_view_pip_opengl.h"
#include "ui/gl/gl_shader.h"
#include "ui/gl/gl_primitives.h"
#include "ui/widgets/shadow.h"
#include "media/streaming/media_streaming_common.h"
#include "base/platform/base_platform_info.h"
#include "styles/style_media_view.h"
#include "styles/style_widgets.h"
namespace Media::View {
namespace {
using namespace Ui::GL;
constexpr auto kRadialLoadingOffset = 4;
constexpr auto kPlaybackOffset = kRadialLoadingOffset + 4;
constexpr auto kVolumeControllerOffset = kPlaybackOffset + 4;
constexpr auto kControlsOffset = kVolumeControllerOffset + 4;
constexpr auto kControlValues = 4 * 4 + 2 * 4;
[[nodiscard]] ShaderPart FragmentAddControlOver() {
return {
.header = R"(
varying vec2 o_texcoord;
uniform float o_opacity;
)",
.body = R"(
vec4 over = texture2D(s_texture, o_texcoord);
result = result * (1. - o_opacity)
+ vec4(over.b, over.g, over.r, over.a) * o_opacity;
)",
};
}
[[nodiscard]] ShaderPart FragmentApplyFade() {
return {
.header = R"(
uniform vec4 fadeColor; // Premultiplied.
)",
.body = R"(
result = result * (1. - fadeColor.a) + fadeColor;
)",
};
}
ShaderPart FragmentSampleShadow() {
return {
.header = R"(
uniform sampler2D h_texture;
uniform vec2 h_size;
uniform vec4 h_extend;
uniform vec4 h_components;
)",
.body = R"(
vec4 extended = vec4( // Left-Bottom-Width-Height rectangle.
roundRect.xy - h_extend.xw,
roundRect.zw + h_extend.xw + h_extend.zy);
vec2 inside = (gl_FragCoord.xy - extended.xy);
vec2 insideOtherCorner = (inside + h_size - extended.zw);
vec4 outsideCorners = step(
vec4(h_components.xy, inside),
vec4(inside, extended.zw - h_components.xy));
vec4 insideCorners = vec4(1.) - outsideCorners;
vec2 linear = outsideCorners.xy * outsideCorners.zw;
vec2 h_size_half = 0.5 * h_size;
vec2 bottomleft = inside * insideCorners.x * insideCorners.y;
vec2 bottomright = vec2(insideOtherCorner.x, inside.y)
* insideCorners.z
* insideCorners.y;
vec2 topright = insideOtherCorner * insideCorners.z * insideCorners.w;
vec2 topleft = vec2(inside.x, insideOtherCorner.y)
* insideCorners.x
* insideCorners.w;
vec2 left = vec2(inside.x, h_size_half.y)
* step(inside.x, h_components.z)
* linear.y;
vec2 bottom = vec2(h_size_half.x, inside.y)
* step(inside.y, h_components.w)
* linear.x;
vec2 right = vec2(insideOtherCorner.x, h_size_half.y)
* step(h_size.x - h_components.z, insideOtherCorner.x)
* linear.y;
vec2 top = vec2(h_size_half.x, insideOtherCorner.y)
* step(h_size.y - h_components.w, insideOtherCorner.y)
* linear.x;
vec2 uv = bottomleft
+ bottomright
+ topleft
+ topright
+ left
+ bottom
+ right
+ top;
result = texture2D(h_texture, uv / h_size);
)",
};
}
ShaderPart FragmentRoundToShadow() {
const auto shadow = FragmentSampleShadow();
return {
.header = R"(
uniform vec4 roundRect;
uniform float roundRadius;
)" + shadow.header + R"(
float roundedCorner() {
vec2 rectHalf = roundRect.zw / 2.;
vec2 rectCenter = roundRect.xy + rectHalf;
vec2 fromRectCenter = abs(gl_FragCoord.xy - rectCenter);
vec2 vectorRadius = vec2(roundRadius + 0.5, roundRadius + 0.5);
vec2 fromCenterWithRadius = fromRectCenter + vectorRadius;
vec2 fromRoundingCenter = max(fromCenterWithRadius, rectHalf)
- rectHalf;
float rounded = length(fromRoundingCenter) - roundRadius;
return 1. - smoothstep(0., 1., rounded);
}
vec4 shadow() {
vec4 result;
)" + shadow.body + R"(
return result;
}
)",
.body = R"(
float round = roundedCorner();
result = result * round + shadow() * (1. - round);
)",
};
}
} // namespace
Pip::RendererGL::RendererGL(not_null<Pip*> owner)
: _owner(owner) {
style::PaletteChanged(
) | rpl::on_next([=] {
_radialImage.invalidate();
_playbackImage.invalidate();
_volumeControllerImage.invalidate();
invalidateControls();
}, _lifetime);
}
void Pip::RendererGL::init(QOpenGLFunctions &f) {
constexpr auto kQuads = 8;
constexpr auto kQuadVertices = kQuads * 4;
constexpr auto kQuadValues = kQuadVertices * 4;
constexpr auto kControlsValues = kControlsCount * kControlValues;
constexpr auto kValues = kQuadValues + kControlsValues;
_contentBuffer.emplace();
_contentBuffer->setUsagePattern(QOpenGLBuffer::DynamicDraw);
_contentBuffer->create();
_contentBuffer->bind();
_contentBuffer->allocate(kValues * sizeof(GLfloat));
_textures.ensureCreated(f);
_argb32Program.emplace();
_texturedVertexShader = LinkProgram(
&*_argb32Program,
VertexShader({
VertexPassTextureCoord(),
}),
FragmentShader({
FragmentSampleARGB32Texture(),
FragmentApplyFade(),
FragmentRoundToShadow(),
})).vertex;
_yuv420Program.emplace();
LinkProgram(
&*_yuv420Program,
_texturedVertexShader,
FragmentShader({
FragmentSampleYUV420Texture(),
FragmentApplyFade(),
FragmentRoundToShadow(),
}));
_nv12Program.emplace();
LinkProgram(
&*_nv12Program,
_texturedVertexShader,
FragmentShader({
FragmentSampleNV12Texture(),
FragmentApplyFade(),
FragmentRoundToShadow(),
}));
_imageProgram.emplace();
LinkProgram(
&*_imageProgram,
VertexShader({
VertexViewportTransform(),
VertexPassTextureCoord(),
}),
FragmentShader({
FragmentSampleARGB32Texture(),
}));
_controlsProgram.emplace();
LinkProgram(
&*_controlsProgram,
VertexShader({
VertexViewportTransform(),
VertexPassTextureCoord(),
VertexPassTextureCoord('o'),
}),
FragmentShader({
FragmentSampleARGB32Texture(),
FragmentAddControlOver(),
FragmentGlobalOpacity(),
}));
createShadowTexture();
}
void Pip::RendererGL::deinit(QOpenGLFunctions *f) {
_radialImage.destroy(f);
_controlsImage.destroy(f);
_playbackImage.destroy(f);
_volumeControllerImage.destroy(f);
_shadowImage.destroy(f);
_textures.destroy(f);
_imageProgram = std::nullopt;
_texturedVertexShader = nullptr;
_argb32Program = std::nullopt;
_yuv420Program = std::nullopt;
_nv12Program = std::nullopt;
_controlsProgram = std::nullopt;
_contentBuffer = std::nullopt;
}
void Pip::RendererGL::createShadowTexture() {
const auto &shadow = PipShadow();
const auto size = 2 * PipShadow().topLeft.size()
+ QSize(st::roundRadiusLarge, st::roundRadiusLarge);
auto image = QImage(
size * style::DevicePixelRatio(),
QImage::Format_ARGB32_Premultiplied);
image.setDevicePixelRatio(style::DevicePixelRatio());
image.fill(Qt::transparent);
{
auto p = QPainter(&image);
Ui::Shadow::paint(
p,
QRect(QPoint(), size).marginsRemoved(shadow.extend),
size.width(),
shadow);
}
_shadowImage.setImage(std::move(image));
}
void Pip::RendererGL::paint(
not_null<QOpenGLWidget*> widget,
QOpenGLFunctions &f) {
const auto factor = widget->devicePixelRatioF();
if (_factor != factor) {
_factor = factor;
_ifactor = int(std::ceil(_factor));
_controlsImage.invalidate();
}
_blendingEnabled = false;
_viewport = widget->size();
_uniformViewport = QVector2D(
_viewport.width() * _factor,
_viewport.height() * _factor);
_f = &f;
_owner->paint(this);
_f = nullptr;
}
std::optional<QColor> Pip::RendererGL::clearColor() {
return QColor(0, 0, 0, 0);
}
void Pip::RendererGL::paintTransformedVideoFrame(
ContentGeometry geometry) {
const auto data = _owner->videoFrameWithInfo();
if (data.format == Streaming::FrameFormat::None) {
return;
}
geometry.rotation = (geometry.rotation + geometry.videoRotation) % 360;
if (data.format == Streaming::FrameFormat::ARGB32) {
Assert(!data.image.isNull());
paintTransformedStaticContent(data.image, geometry);
return;
}
Assert(!data.yuv->size.isEmpty());
const auto program = (data.format == Streaming::FrameFormat::NV12)
? &*_nv12Program
: &*_yuv420Program;
program->bind();
const auto nv12 = (data.format == Streaming::FrameFormat::NV12);
const auto yuv = data.yuv;
const auto nv12changed = (_chromaNV12 != nv12);
const auto upload = (_trackFrameIndex != data.index);
_trackFrameIndex = data.index;
_f->glActiveTexture(GL_TEXTURE0);
_textures.bind(*_f, 1);
if (upload) {
_f->glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
uploadTexture(
GL_ALPHA,
GL_ALPHA,
yuv->size,
_lumaSize,
yuv->y.stride,
yuv->y.data);
_lumaSize = yuv->size;
}
_f->glActiveTexture(GL_TEXTURE1);
_textures.bind(*_f, 2);
if (upload) {
uploadTexture(
nv12 ? GL_RG : GL_ALPHA,
nv12 ? GL_RG : GL_ALPHA,
yuv->chromaSize,
nv12changed ? QSize() : _chromaSize,
yuv->u.stride / (nv12 ? 2 : 1),
yuv->u.data);
if (nv12) {
_chromaSize = yuv->chromaSize;
_f->glPixelStorei(GL_UNPACK_ALIGNMENT, 4);
}
_chromaNV12 = nv12;
}
if (!nv12) {
_f->glActiveTexture(GL_TEXTURE2);
_textures.bind(*_f, 3);
if (upload) {
uploadTexture(
GL_ALPHA,
GL_ALPHA,
yuv->chromaSize,
_chromaSize,
yuv->v.stride,
yuv->v.data);
_chromaSize = yuv->chromaSize;
_f->glPixelStorei(GL_UNPACK_ALIGNMENT, 4);
}
}
program->setUniformValue("y_texture", GLint(0));
if (nv12) {
program->setUniformValue("uv_texture", GLint(1));
} else {
program->setUniformValue("u_texture", GLint(1));
program->setUniformValue("v_texture", GLint(2));
}
paintTransformedContent(program, geometry);
}
void Pip::RendererGL::paintTransformedStaticContent(
const QImage &image,
ContentGeometry geometry) {
_argb32Program->bind();
_f->glActiveTexture(GL_TEXTURE0);
_textures.bind(*_f, 0);
const auto cacheKey = image.cacheKey();
const auto upload = (_cacheKey != cacheKey);
if (upload) {
_cacheKey = cacheKey;
const auto stride = image.bytesPerLine() / 4;
const auto data = image.constBits();
uploadTexture(
Ui::GL::kFormatRGBA,
Ui::GL::kFormatRGBA,
image.size(),
_rgbaSize,
stride,
data);
_rgbaSize = image.size();
}
_argb32Program->setUniformValue("s_texture", GLint(0));
paintTransformedContent(&*_argb32Program, geometry);
}
void Pip::RendererGL::paintTransformedContent(
not_null<QOpenGLShaderProgram*> program,
ContentGeometry geometry) {
std::array<std::array<GLfloat, 2>, 4> rect = { {
{ { -1.f, 1.f } },
{ { 1.f, 1.f } },
{ { 1.f, -1.f } },
{ { -1.f, -1.f } },
} };
if (const auto shift = (geometry.rotation / 90); shift != 0) {
std::rotate(begin(rect), begin(rect) + shift, end(rect));
}
const auto xscale = 1.f / geometry.inner.width();
const auto yscale = 1.f / geometry.inner.height();
const GLfloat coords[] = {
rect[0][0], rect[0][1],
-geometry.inner.x() * xscale,
-geometry.inner.y() * yscale,
rect[1][0], rect[1][1],
(geometry.outer.width() - geometry.inner.x()) * xscale,
-geometry.inner.y() * yscale,
rect[2][0], rect[2][1],
(geometry.outer.width() - geometry.inner.x()) * xscale,
(geometry.outer.height() - geometry.inner.y()) * yscale,
rect[3][0], rect[3][1],
-geometry.inner.x() * xscale,
(geometry.outer.height() - geometry.inner.y()) * yscale,
};
_contentBuffer->bind();
_contentBuffer->write(0, coords, sizeof(coords));
const auto rgbaFrame = _chromaSize.isEmpty();
_f->glActiveTexture(rgbaFrame ? GL_TEXTURE1 : GL_TEXTURE3);
_shadowImage.bind(*_f);
const auto globalFactor = style::DevicePixelRatio();
const auto fadeAlpha = st::radialBg->c.alphaF() * geometry.fade;
const auto roundRect = transformRect(RoundingRect(geometry));
program->setUniformValue("roundRect", Uniform(roundRect));
program->setUniformValue("h_texture", GLint(rgbaFrame ? 1 : 3));
program->setUniformValue("h_size", QSizeF(_shadowImage.image().size()));
program->setUniformValue("h_extend", QVector4D(
PipShadow().extend.left() * globalFactor,
PipShadow().extend.top() * globalFactor,
PipShadow().extend.right() * globalFactor,
PipShadow().extend.bottom() * globalFactor));
program->setUniformValue("h_components", QVector4D(
float(PipShadow().topLeft.width() * globalFactor),
float(PipShadow().topLeft.height() * globalFactor),
float(PipShadow().left.width() * globalFactor),
float(PipShadow().top.height() * globalFactor)));
program->setUniformValue(
"roundRadius",
GLfloat(st::roundRadiusLarge * _factor));
program->setUniformValue("fadeColor", QVector4D(
float(st::radialBg->c.redF() * fadeAlpha),
float(st::radialBg->c.greenF() * fadeAlpha),
float(st::radialBg->c.blueF() * fadeAlpha),
float(fadeAlpha)));
FillTexturedRectangle(*_f, &*program);
}
void Pip::RendererGL::uploadTexture(
GLint internalformat,
GLint format,
QSize size,
QSize hasSize,
int stride,
const void *data) const {
_f->glPixelStorei(GL_UNPACK_ROW_LENGTH, stride);
if (hasSize != size) {
_f->glTexImage2D(
GL_TEXTURE_2D,
0,
internalformat,
size.width(),
size.height(),
0,
format,
GL_UNSIGNED_BYTE,
data);
} else {
_f->glTexSubImage2D(
GL_TEXTURE_2D,
0,
0,
0,
size.width(),
size.height(),
format,
GL_UNSIGNED_BYTE,
data);
}
_f->glPixelStorei(GL_UNPACK_ROW_LENGTH, 0);
}
void Pip::RendererGL::paintRadialLoading(
QRect inner,
float64 controlsShown) {
paintUsingRaster(_radialImage, inner, [&](QPainter &&p) {
// Raster renderer paints content, then radial loading, then fade.
// Here we paint fade together with the content, so we should emulate
// radial loading being under the fade.
//
// The loading background is the same color as the fade (radialBg),
// so nothing should be done with it. But the fade should be added
// to the radial loading line color (radialFg).
const auto newInner = QRect(QPoint(), inner.size());
const auto fg = st::radialFg->c;
const auto fade = st::radialBg->c;
const auto fadeAlpha = controlsShown * fade.alphaF();
const auto fgAlpha = 1. - fadeAlpha;
const auto color = (fadeAlpha == 0.) ? fg : QColor(
int(base::SafeRound(fg.red() * fgAlpha + fade.red() * fadeAlpha)),
int(base::SafeRound(fg.green() * fgAlpha + fade.green() * fadeAlpha)),
int(base::SafeRound(fg.blue() * fgAlpha + fade.blue() * fadeAlpha)),
fg.alpha());
_owner->paintRadialLoadingContent(p, newInner, color);
}, kRadialLoadingOffset, true);
}
void Pip::RendererGL::paintPlayback(QRect outer, float64 shown) {
paintUsingRaster(_playbackImage, outer, [&](QPainter &&p) {
const auto newOuter = QRect(QPoint(), outer.size());
_owner->paintPlaybackContent(p, newOuter, shown);
}, kPlaybackOffset, true);
}
void Pip::RendererGL::paintVolumeController(QRect outer, float64 shown) {
paintUsingRaster(_volumeControllerImage, outer, [&](QPainter &&p) {
const auto newOuter = QRect(QPoint(), outer.size());
_owner->paintVolumeControllerContent(p, newOuter, shown);
}, kVolumeControllerOffset, true);
}
void Pip::RendererGL::paintButtonsStart() {
validateControls();
_f->glActiveTexture(GL_TEXTURE0);
_controlsImage.bind(*_f);
toggleBlending(true);
}
void Pip::RendererGL::paintButton(
const Button &button,
int outerWidth,
float64 shown,
float64 over,
const style::icon &icon,
const style::icon &iconOver) {
const auto tryIndex = [&](int stateIndex) -> std::optional<Control> {
const auto result = ControlMeta(button.state, stateIndex);
return (result.icon == &icon && result.iconOver == &iconOver)
? std::make_optional(result)
: std::nullopt;
};
const auto meta = tryIndex(0)
? *tryIndex(0)
: tryIndex(1)
? *tryIndex(1)
: *tryIndex(2);
Assert(meta.icon == &icon && meta.iconOver == &iconOver);
const auto offset = kControlsOffset + (meta.index * kControlValues) / 4;
const auto iconRect = _controlsImage.texturedRect(
button.icon,
_controlsTextures[meta.index * 2 + 0]);
const auto iconOverRect = _controlsImage.texturedRect(
button.icon,
_controlsTextures[meta.index * 2 + 1]);
const auto iconGeometry = transformRect(iconRect.geometry);
const GLfloat coords[] = {
iconGeometry.left(), iconGeometry.top(),
iconRect.texture.left(), iconRect.texture.bottom(),
iconGeometry.right(), iconGeometry.top(),
iconRect.texture.right(), iconRect.texture.bottom(),
iconGeometry.right(), iconGeometry.bottom(),
iconRect.texture.right(), iconRect.texture.top(),
iconGeometry.left(), iconGeometry.bottom(),
iconRect.texture.left(), iconRect.texture.top(),
iconOverRect.texture.left(), iconOverRect.texture.bottom(),
iconOverRect.texture.right(), iconOverRect.texture.bottom(),
iconOverRect.texture.right(), iconOverRect.texture.top(),
iconOverRect.texture.left(), iconOverRect.texture.top(),
};
_contentBuffer->bind();
_contentBuffer->write(
offset * 4 * sizeof(GLfloat),
coords,
sizeof(coords));
_controlsProgram->bind();
_controlsProgram->setUniformValue("o_opacity", GLfloat(over));
_controlsProgram->setUniformValue("g_opacity", GLfloat(shown));
_controlsProgram->setUniformValue("viewport", _uniformViewport);
GLint overTexcoord = _controlsProgram->attributeLocation("o_texcoordIn");
_f->glVertexAttribPointer(
overTexcoord,
2,
GL_FLOAT,
GL_FALSE,
2 * sizeof(GLfloat),
reinterpret_cast<const void*>((offset + 4) * 4 * sizeof(GLfloat)));
_f->glEnableVertexAttribArray(overTexcoord);
FillTexturedRectangle(*_f, &*_controlsProgram, offset);
_f->glDisableVertexAttribArray(overTexcoord);
}
auto Pip::RendererGL::ControlMeta(OverState control, int index)
-> Control {
Expects(index >= 0);
switch (control) {
case OverState::Close: Assert(index < 1); return {
0,
&st::pipCloseIcon,
&st::pipCloseIconOver,
};
case OverState::Enlarge: Assert(index < 1); return {
1,
&st::pipEnlargeIcon,
&st::pipEnlargeIconOver,
};
case OverState::VolumeToggle: Assert(index < 3); return {
(2 + index),
(index == 0
? &st::pipVolumeIcon0
: (index == 1)
? &st::pipVolumeIcon1
: &st::pipVolumeIcon2),
(index == 0
? &st::pipVolumeIcon0Over
: (index == 1)
? &st::pipVolumeIcon1Over
: &st::pipVolumeIcon2Over),
};
case OverState::Other: Assert(index < 2); return {
(5 + index),
(index ? &st::pipPauseIcon : &st::pipPlayIcon),
(index ? &st::pipPauseIconOver : &st::pipPlayIconOver),
};
}
Unexpected("Control value in Pip::RendererGL::ControlIndex.");
}
void Pip::RendererGL::validateControls() {
if (!_controlsImage.image().isNull()) {
return;
}
const auto metas = {
ControlMeta(OverState::Close),
ControlMeta(OverState::Enlarge),
ControlMeta(OverState::VolumeToggle),
ControlMeta(OverState::VolumeToggle, 1),
ControlMeta(OverState::VolumeToggle, 2),
ControlMeta(OverState::Other),
ControlMeta(OverState::Other, 1),
};
auto maxWidth = 0;
auto fullHeight = 0;
for (const auto &meta : metas) {
Assert(meta.icon->size() == meta.iconOver->size());
maxWidth = std::max(meta.icon->width(), maxWidth);
fullHeight += 2 * meta.icon->height();
}
auto image = QImage(
QSize(maxWidth, fullHeight) * _ifactor,
QImage::Format_ARGB32_Premultiplied);
image.fill(Qt::transparent);
image.setDevicePixelRatio(_ifactor);
{
auto p = QPainter(&image);
auto index = 0;
auto height = 0;
const auto paint = [&](not_null<const style::icon*> icon) {
icon->paint(p, 0, height, maxWidth);
_controlsTextures[index++] = QRect(
QPoint(0, height) * _ifactor,
icon->size() * _ifactor);
height += icon->height();
};
for (const auto &meta : metas) {
paint(meta.icon);
paint(meta.iconOver);
}
}
_controlsImage.setImage(std::move(image));
}
void Pip::RendererGL::invalidateControls() {
_controlsImage.invalidate();
ranges::fill(_controlsTextures, QRect());
}
void Pip::RendererGL::paintUsingRaster(
Ui::GL::Image &image,
QRect rect,
Fn<void(QPainter&&)> method,
int bufferOffset,
bool transparent) {
auto raster = image.takeImage();
const auto size = rect.size() * _ifactor;
if (raster.width() < size.width() || raster.height() < size.height()) {
raster = QImage(size, QImage::Format_ARGB32_Premultiplied);
raster.setDevicePixelRatio(_factor);
if (!transparent
&& (raster.width() > size.width()
|| raster.height() > size.height())) {
raster.fill(Qt::transparent);
}
} else if (raster.devicePixelRatio() != _ifactor) {
raster.setDevicePixelRatio(_ifactor);
}
if (transparent) {
raster.fill(Qt::transparent);
}
method(QPainter(&raster));
_f->glActiveTexture(GL_TEXTURE0);
image.setImage(std::move(raster), size);
image.bind(*_f);
const auto textured = image.texturedRect(rect, QRect(QPoint(), size));
const auto geometry = transformRect(textured.geometry);
const GLfloat coords[] = {
geometry.left(), geometry.top(),
textured.texture.left(), textured.texture.bottom(),
geometry.right(), geometry.top(),
textured.texture.right(), textured.texture.bottom(),
geometry.right(), geometry.bottom(),
textured.texture.right(), textured.texture.top(),
geometry.left(), geometry.bottom(),
textured.texture.left(), textured.texture.top(),
};
_contentBuffer->bind();
_contentBuffer->write(
bufferOffset * 4 * sizeof(GLfloat),
coords,
sizeof(coords));
_imageProgram->bind();
_imageProgram->setUniformValue("viewport", _uniformViewport);
_imageProgram->setUniformValue("s_texture", GLint(0));
_imageProgram->setUniformValue("g_opacity", GLfloat(1));
toggleBlending(transparent);
FillTexturedRectangle(*_f, &*_imageProgram, bufferOffset);
}
void Pip::RendererGL::toggleBlending(bool enabled) {
if (_blendingEnabled == enabled) {
return;
} else if (enabled) {
_f->glEnable(GL_BLEND);
_f->glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA);
} else {
_f->glDisable(GL_BLEND);
}
_blendingEnabled = enabled;
}
QRect Pip::RendererGL::RoundingRect(ContentGeometry geometry) {
const auto inner = geometry.inner;
const auto attached = geometry.attached;
const auto added = std::max({
st::roundRadiusLarge,
inner.x(),
inner.y(),
geometry.outer.width() - inner.x() - inner.width(),
geometry.outer.height() - inner.y() - inner.height(),
PipShadow().topLeft.width(),
PipShadow().topLeft.height(),
PipShadow().topRight.width(),
PipShadow().topRight.height(),
PipShadow().bottomRight.width(),
PipShadow().bottomRight.height(),
PipShadow().bottomLeft.width(),
PipShadow().bottomLeft.height(),
});
return geometry.inner.marginsAdded({
(attached & RectPart::Left) ? added : 0,
(attached & RectPart::Top) ? added : 0,
(attached & RectPart::Right) ? added : 0,
(attached & RectPart::Bottom) ? added : 0,
});
}
Rect Pip::RendererGL::transformRect(const Rect &raster) const {
return TransformRect(raster, _viewport, _factor);
}
Rect Pip::RendererGL::transformRect(const QRectF &raster) const {
return TransformRect(raster, _viewport, _factor);
}
Rect Pip::RendererGL::transformRect(const QRect &raster) const {
return TransformRect(Rect(raster), _viewport, _factor);
}
} // namespace Media::View

View File

@@ -0,0 +1,128 @@
/*
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 "media/view/media_view_pip_renderer.h"
#include "ui/gl/gl_image.h"
#include "ui/gl/gl_primitives.h"
#include <QOpenGLBuffer>
namespace Media::View {
class Pip::RendererGL final : public Pip::Renderer {
public:
explicit RendererGL(not_null<Pip*> owner);
void init(QOpenGLFunctions &f) override;
void deinit(QOpenGLFunctions *f) override;
void paint(
not_null<QOpenGLWidget*> widget,
QOpenGLFunctions &f) override;
std::optional<QColor> clearColor() override;
private:
struct Control {
int index = -1;
not_null<const style::icon*> icon;
not_null<const style::icon*> iconOver;
};
void createShadowTexture();
void paintTransformedVideoFrame(ContentGeometry geometry) override;
void paintTransformedStaticContent(
const QImage &image,
ContentGeometry geometry) override;
void paintTransformedContent(
not_null<QOpenGLShaderProgram*> program,
ContentGeometry geometry);
void paintRadialLoading(
QRect inner,
float64 controlsShown) override;
void paintButtonsStart() override;
void paintButton(
const Button &button,
int outerWidth,
float64 shown,
float64 over,
const style::icon &icon,
const style::icon &iconOver) override;
void paintPlayback(QRect outer, float64 shown) override;
void paintVolumeController(QRect outer, float64 shown) override;
void paintUsingRaster(
Ui::GL::Image &image,
QRect rect,
Fn<void(QPainter&&)> method,
int bufferOffset,
bool transparent = false);
void validateControls();
void invalidateControls();
void toggleBlending(bool enabled);
[[nodiscard]] QRect RoundingRect(ContentGeometry geometry);
[[nodiscard]] Ui::GL::Rect transformRect(const QRect &raster) const;
[[nodiscard]] Ui::GL::Rect transformRect(const QRectF &raster) const;
[[nodiscard]] Ui::GL::Rect transformRect(
const Ui::GL::Rect &raster) const;
void uploadTexture(
GLint internalformat,
GLint format,
QSize size,
QSize hasSize,
int stride,
const void *data) const;
const not_null<Pip*> _owner;
QOpenGLFunctions *_f = nullptr;
QSize _viewport;
float _factor = 1.;
int _ifactor = 1;
QVector2D _uniformViewport;
std::optional<QOpenGLBuffer> _contentBuffer;
std::optional<QOpenGLShaderProgram> _imageProgram;
std::optional<QOpenGLShaderProgram> _controlsProgram;
QOpenGLShader *_texturedVertexShader = nullptr;
std::optional<QOpenGLShaderProgram> _argb32Program;
std::optional<QOpenGLShaderProgram> _yuv420Program;
std::optional<QOpenGLShaderProgram> _nv12Program;
Ui::GL::Textures<4> _textures;
QSize _rgbaSize;
QSize _lumaSize;
QSize _chromaSize;
quint64 _cacheKey = 0;
int _trackFrameIndex = 0;
bool _chromaNV12 = false;
Ui::GL::Image _radialImage;
Ui::GL::Image _controlsImage;
Ui::GL::Image _playbackImage;
Ui::GL::Image _volumeControllerImage;
Ui::GL::Image _shadowImage;
static constexpr auto kControlsCount = 7;
[[nodiscard]] static Control ControlMeta(
OverState control,
int index = 0);
std::array<QRect, kControlsCount * 2> _controlsTextures;
bool _blendingEnabled = false;
rpl::lifetime _lifetime;
};
} // namespace Media::View

View File

@@ -0,0 +1,228 @@
/*
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 "media/view/media_view_pip_raster.h"
#include "ui/image/image_prepare.h"
#include "ui/widgets/shadow.h"
#include "ui/painter.h"
#include "styles/style_media_view.h"
#include "styles/style_widgets.h"
namespace Media::View {
namespace {
[[nodiscard]] Streaming::FrameRequest UnrotateRequest(
const Streaming::FrameRequest &request,
int rotation) {
if (!rotation) {
return request;
}
const auto unrotatedCorner = [&](int index) {
using namespace Images;
switch (index) {
case kTopLeft:
return (rotation == 90)
? kBottomLeft
: (rotation == 180)
? kBottomRight
: kTopRight;
case kTopRight:
return (rotation == 90)
? kTopLeft
: (rotation == 180)
? kBottomLeft
: kBottomRight;
case kBottomRight:
return (rotation == 90)
? kTopRight
: (rotation == 180)
? kTopLeft
: kBottomLeft;
case kBottomLeft:
return (rotation == 90)
? kBottomRight
: (rotation == 180)
? kTopRight
: kTopLeft;
}
Unexpected("Corner in rotateCorner.");
};
auto result = request;
result.outer = FlipSizeByRotation(request.outer, rotation);
result.resize = FlipSizeByRotation(request.resize, rotation);
auto rounding = result.rounding;
for (auto i = 0; i != 4; ++i) {
result.rounding.p[unrotatedCorner(i)] = rounding.p[i];
}
return result;
}
} // namespace
Pip::RendererSW::RendererSW(not_null<Pip*> owner)
: _owner(owner)
, _roundRect(ImageRoundRadius::Large, st::radialBg) {
}
void Pip::RendererSW::paintFallback(
Painter &p,
const QRegion &clip,
Ui::GL::Backend backend) {
_p = &p;
_clip = &clip;
_clipOuter = clip.boundingRect();
_owner->paint(this);
_p = nullptr;
_clip = nullptr;
}
void Pip::RendererSW::paintTransformedVideoFrame(
ContentGeometry geometry) {
paintTransformedImage(
_owner->videoFrame(frameRequest(geometry)),
geometry);
}
void Pip::RendererSW::paintTransformedStaticContent(
const QImage &image,
ContentGeometry geometry) {
paintTransformedImage(
staticContentByRequest(image, frameRequest(geometry)),
geometry);
}
void Pip::RendererSW::paintFade(ContentGeometry geometry) const {
using Part = RectPart;
const auto sides = geometry.attached;
const auto rounded = RectPart(0)
| ((sides & (Part::Top | Part::Left)) ? Part(0) : Part::TopLeft)
| ((sides & (Part::Top | Part::Right)) ? Part(0) : Part::TopRight)
| ((sides & (Part::Bottom | Part::Right))
? Part(0)
: Part::BottomRight)
| ((sides & (Part::Bottom | Part::Left))
? Part(0)
: Part::BottomLeft);
_roundRect.paintSomeRounded(
*_p,
geometry.inner,
rounded | Part::NoTopBottom | Part::Top | Part::Bottom);
}
void Pip::RendererSW::paintButtonsStart() {
}
void Pip::RendererSW::paintButton(
const Button &button,
int outerWidth,
float64 shown,
float64 over,
const style::icon &icon,
const style::icon &iconOver) {
if (over < 1.) {
_p->setOpacity(shown);
icon.paint(*_p, button.icon.x(), button.icon.y(), outerWidth);
}
if (over > 0.) {
_p->setOpacity(over * shown);
iconOver.paint(*_p, button.icon.x(), button.icon.y(), outerWidth);
}
}
Pip::FrameRequest Pip::RendererSW::frameRequest(
ContentGeometry geometry) const {
using namespace Images;
auto result = FrameRequest();
result.outer = geometry.inner.size() * style::DevicePixelRatio();
result.resize = result.outer;
result.rounding = CornersMaskRef(CornersMask(ImageRoundRadius::Large));
if (geometry.attached & (RectPart::Top | RectPart::Left)) {
result.rounding.p[kTopLeft] = nullptr;
}
if (geometry.attached & (RectPart::Top | RectPart::Right)) {
result.rounding.p[kTopRight] = nullptr;
}
if (geometry.attached & (RectPart::Bottom | RectPart::Left)) {
result.rounding.p[kBottomLeft] = nullptr;
}
if (geometry.attached & (RectPart::Bottom | RectPart::Right)) {
result.rounding.p[kBottomRight] = nullptr;
}
return UnrotateRequest(result, geometry.rotation);
}
QImage Pip::RendererSW::staticContentByRequest(
const QImage &image,
const FrameRequest &request) {
if (request.resize.isEmpty()) {
return QImage();
} else if (!_preparedStaticContent.isNull()
&& _preparedStaticRequest == request
&& image.cacheKey() == _preparedStaticKey) {
return _preparedStaticContent;
}
_preparedStaticKey = image.cacheKey();
_preparedStaticRequest = request;
_preparedStaticContent = Images::Round(
Images::Prepare(
image,
request.resize,
{ .outer = request.outer / style::DevicePixelRatio() }),
request.rounding);
return _preparedStaticContent;
}
void Pip::RendererSW::paintTransformedImage(
const QImage &image,
ContentGeometry geometry) {
const auto rect = geometry.inner;
const auto rotation = geometry.rotation;
if (geometry.useTransparency) {
Ui::Shadow::paint(
*_p,
rect,
geometry.outer.width(),
PipShadow());
}
if (UsePainterRotation(rotation)) {
if (rotation) {
_p->save();
_p->rotate(rotation);
}
PainterHighQualityEnabler hq(*_p);
_p->drawImage(RotatedRect(rect, rotation), image);
if (rotation) {
_p->restore();
}
} else {
_p->drawImage(rect, RotateFrameImage(image, rotation));
}
if (geometry.fade > 0) {
_p->setOpacity(geometry.fade);
paintFade(geometry);
}
}
void Pip::RendererSW::paintRadialLoading(
QRect inner,
float64 controlsShown) {
_owner->paintRadialLoadingContent(*_p, inner, st::radialFg->c);
}
void Pip::RendererSW::paintPlayback(QRect outer, float64 shown) {
_owner->paintPlaybackContent(*_p, outer, shown);
}
void Pip::RendererSW::paintVolumeController(QRect outer, float64 shown) {
_owner->paintVolumeControllerContent(*_p, outer, shown);
}
} // namespace Media::View

View File

@@ -0,0 +1,66 @@
/*
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 "media/view/media_view_pip_renderer.h"
namespace Media::View {
class Pip::RendererSW final : public Pip::Renderer {
public:
explicit RendererSW(not_null<Pip*> owner);
void paintFallback(
Painter &p,
const QRegion &clip,
Ui::GL::Backend backend) override;
private:
void paintTransformedVideoFrame(ContentGeometry geometry) override;
void paintTransformedStaticContent(
const QImage &image,
ContentGeometry geometry) override;
void paintTransformedImage(
const QImage &image,
ContentGeometry geometry);
void paintRadialLoading(
QRect inner,
float64 controlsShown) override;
void paintButtonsStart() override;
void paintButton(
const Button &button,
int outerWidth,
float64 shown,
float64 over,
const style::icon &icon,
const style::icon &iconOver) override;
void paintPlayback(QRect outer, float64 shown) override;
void paintVolumeController(QRect outer, float64 shown) override;
void paintFade(ContentGeometry geometry) const;
[[nodiscard]] FrameRequest frameRequest(ContentGeometry geometry) const;
[[nodiscard]] QImage staticContentByRequest(
const QImage &image,
const FrameRequest &request);
const not_null<Pip*> _owner;
Painter *_p = nullptr;
const QRegion *_clip = nullptr;
QRect _clipOuter;
Ui::RoundRect _roundRect;
QImage _preparedStaticContent;
FrameRequest _preparedStaticRequest;
qint64 _preparedStaticKey = 0;
};
} // namespace Media::View

View File

@@ -0,0 +1,37 @@
/*
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 "media/view/media_view_pip.h"
#include "ui/gl/gl_surface.h"
namespace Media::View {
class Pip::Renderer : public Ui::GL::Renderer {
public:
virtual void paintTransformedVideoFrame(ContentGeometry geometry) = 0;
virtual void paintTransformedStaticContent(
const QImage &image,
ContentGeometry geometry) = 0;
virtual void paintRadialLoading(
QRect inner,
float64 controlsShown) = 0;
virtual void paintButtonsStart() = 0;
virtual void paintButton(
const Button &button,
int outerWidth,
float64 shown,
float64 over,
const style::icon &icon,
const style::icon &iconOver) = 0;
virtual void paintPlayback(QRect outer, float64 shown) = 0;
virtual void paintVolumeController(QRect outer, float64 shown) = 0;
};
} // namespace Media::View

View File

@@ -0,0 +1,452 @@
/*
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 "media/view/media_view_playback_controls.h"
#include "media/audio/media_audio.h"
#include "media/player/media_player_button.h"
#include "media/player/media_player_dropdown.h"
#include "media/view/media_view_playback_progress.h"
#include "ui/widgets/labels.h"
#include "ui/widgets/continuous_sliders.h"
#include "ui/effects/fade_animation.h"
#include "ui/widgets/buttons.h"
#include "ui/widgets/menu/menu_item_base.h"
#include "ui/text/format_values.h"
#include "ui/cached_round_corners.h"
#include "lang/lang_keys.h"
#include "styles/style_media_view.h"
namespace Media {
namespace View {
PlaybackControls::PlaybackControls(
QWidget *parent,
not_null<Delegate*> delegate)
: RpWidget(parent)
, _delegate(delegate)
, _speedControllable(Media::Audio::SupportsSpeedControl())
, _qualitiesList(_delegate->playbackControlsQualities())
, _playPauseResume(this, st::mediaviewPlayButton)
, _playbackSlider(this, st::mediaviewPlayback)
, _playbackProgress(std::make_unique<PlaybackProgress>())
, _volumeToggle(this, st::mediaviewVolumeToggle)
, _volumeController(this, st::mediaviewPlayback)
, _speedToggle((_speedControllable || !_qualitiesList.empty())
? object_ptr<Player::SettingsButton>(this, st::mediaviewSpeedButton)
: nullptr)
, _fullScreenToggle(this, st::mediaviewFullScreenButton)
, _pictureInPicture(this, st::mediaviewPipButton)
, _playedAlready(this, st::mediaviewPlayProgressLabel)
, _toPlayLeft(this, st::mediaviewPlayProgressLabel)
, _speedController(_speedToggle
? std::make_unique<Player::SpeedController>(
_speedToggle.data(),
_speedToggle->st(),
parent,
[=](bool) {},
(_speedControllable
? [=](bool lastNonDefault) {
return speedLookup(lastNonDefault);
}
: Fn<float64(bool)>()),
(_speedControllable
? [=](float64 speed) { saveSpeed(speed); }
: Fn<void(float64)>()),
_qualitiesList,
[=] { return _delegate->playbackControlsCurrentQuality(); },
[=](int quality) { saveQuality(quality); })
: nullptr)
, _fadeAnimation(std::make_unique<Ui::FadeAnimation>(this)) {
_fadeAnimation->show();
_fadeAnimation->setFinishedCallback([=] {
fadeFinished();
});
_fadeAnimation->setUpdatedCallback([=](float64 opacity) {
fadeUpdated(opacity);
});
_speedToggle->setSpeed(_speedControllable
? _delegate->playbackControlsCurrentSpeed(false)
: 1.);
updateSpeedToggleQuality();
if (const auto controller = _speedController.get()) {
controller->menuToggledValue(
) | rpl::on_next([=](bool toggled) {
_speedToggle->setActive(toggled);
}, _speedToggle->lifetime());
}
_pictureInPicture->addClickHandler([=] {
_delegate->playbackControlsToPictureInPicture();
});
_volumeController->setValue(_delegate->playbackControlsCurrentVolume());
_volumeController->setChangeProgressCallback([=](float64 value) {
_delegate->playbackControlsVolumeChanged(value);
updateVolumeToggleIcon();
});
_volumeController->setChangeFinishedCallback([=](float64) {
_delegate->playbackControlsVolumeChangeFinished();
});
updateVolumeToggleIcon();
_volumeToggle->setClickedCallback([=] {
_delegate->playbackControlsVolumeToggled();
_volumeController->setValue(_delegate->playbackControlsCurrentVolume());
updateVolumeToggleIcon();
});
_playPauseResume->addClickHandler([=] {
if (_showPause) {
_delegate->playbackControlsPause();
} else {
_delegate->playbackControlsPlay();
}
});
_fullScreenToggle->addClickHandler([=] {
if (_inFullScreen) {
_delegate->playbackControlsFromFullScreen();
} else {
_delegate->playbackControlsToFullScreen();
}
});
_playbackProgress->setValueChangedCallback([=](
float64 value,
float64 receivedTill) {
_playbackSlider->setValue(value, receivedTill);
});
_playbackSlider->setChangeProgressCallback([=](float64 value) {
_playbackProgress->setValue(value, false);
// This may destroy PlaybackControls.
handleSeekProgress(value);
});
_playbackSlider->setChangeFinishedCallback([=](float64 value) {
_playbackProgress->setValue(value, false);
handleSeekFinished(value);
});
}
void PlaybackControls::handleSeekProgress(float64 progress) {
if (!_lastDurationMs) return;
const auto positionMs = std::clamp(
static_cast<crl::time>(progress * _lastDurationMs),
crl::time(0),
_lastDurationMs);
if (_seekPositionMs != positionMs) {
_seekPositionMs = positionMs;
refreshTimeTexts();
// This may destroy PlaybackControls.
_delegate->playbackControlsSeekProgress(positionMs);
}
}
void PlaybackControls::handleSeekFinished(float64 progress) {
if (!_lastDurationMs) return;
const auto positionMs = std::clamp(
static_cast<crl::time>(progress * _lastDurationMs),
crl::time(0),
_lastDurationMs);
_seekPositionMs = -1;
_delegate->playbackControlsSeekFinished(positionMs);
refreshTimeTexts();
}
template <typename Callback>
void PlaybackControls::startFading(Callback start) {
if (!_fadeAnimation->animating()) {
showChildren();
_playbackSlider->disablePaint(true);
_volumeController->disablePaint(true);
_childrenHidden = false;
}
start();
if (_fadeAnimation->animating()) {
for (const auto child : children()) {
if (child->isWidgetType()
&& child != _playbackSlider
&& child != _volumeController) {
static_cast<QWidget*>(child)->hide();
}
}
_childrenHidden = true;
} else {
fadeFinished();
}
_playbackSlider->disablePaint(false);
_volumeController->disablePaint(false);
}
void PlaybackControls::showAnimated() {
startFading([this]() {
_fadeAnimation->fadeIn(st::mediaviewShowDuration);
});
}
void PlaybackControls::hideAnimated() {
startFading([this]() {
_fadeAnimation->fadeOut(st::mediaviewHideDuration);
});
}
void PlaybackControls::fadeFinished() {
fadeUpdated(_fadeAnimation->visible() ? 1. : 0.);
}
void PlaybackControls::fadeUpdated(float64 opacity) {
_playbackSlider->setFadeOpacity(opacity);
_volumeController->setFadeOpacity(opacity);
}
float64 PlaybackControls::speedLookup(bool lastNonDefault) const {
return _delegate->playbackControlsCurrentSpeed(lastNonDefault);
}
void PlaybackControls::saveSpeed(float64 speed) {
_speedToggle->setSpeed(speed);
_delegate->playbackControlsSpeedChanged(speed);
}
void PlaybackControls::saveQuality(int quality) {
_speedToggle->setQuality(_qualitiesList.empty() ? 0 : quality);
_delegate->playbackControlsQualityChanged(quality);
}
void PlaybackControls::updateSpeedToggleQuality() {
const auto quality = _delegate->playbackControlsCurrentQuality();
_speedToggle->setQuality(_qualitiesList.empty() ? 0 : quality.height);
}
void PlaybackControls::updatePlaybackSpeed(float64 speed) {
DEBUG_LOG(("Media playback speed: update to %1.").arg(speed));
_delegate->playbackControlsSpeedChanged(speed);
resizeEvent(nullptr);
}
void PlaybackControls::updatePlayback(const Player::TrackState &state) {
updatePlayPauseResumeState(state);
_playbackProgress->updateState(state, countDownloadedTillPercent(state));
updateTimeTexts(state);
}
void PlaybackControls::updateVolumeToggleIcon() {
const auto volume = _delegate->playbackControlsCurrentVolume();
_volumeToggle->setIconOverride([&] {
return (volume <= 0.)
? nullptr
: (volume < 1 / 2.)
? &st::mediaviewVolumeIcon1
: &st::mediaviewVolumeIcon2;
}(), [&] {
return (volume <= 0.)
? nullptr
: (volume < 1 / 2.)
? &st::mediaviewVolumeIcon1Over
: &st::mediaviewVolumeIcon2Over;
}());
}
float64 PlaybackControls::countDownloadedTillPercent(
const Player::TrackState &state) const {
if (_loadingReady > 0 && _loadingReady == _loadingTotal) {
return 1.;
}
const auto header = state.fileHeaderSize;
if (!header || _loadingReady <= header || _loadingTotal <= header) {
return 0.;
}
return (_loadingReady - header) / float64(_loadingTotal - header);
}
void PlaybackControls::setLoadingProgress(int64 ready, int64 total) {
if (_loadingReady == ready && _loadingTotal == total) {
return;
}
_loadingReady = ready;
_loadingTotal = total;
if (_loadingReady != 0 && _loadingReady != _loadingTotal) {
if (!_downloadProgress) {
_downloadProgress.create(this, st::mediaviewPlayProgressLabel);
_downloadProgress->setVisible(!_fadeAnimation->animating());
_loadingPercent = -1;
}
const auto progress = total ? (ready / float64(total)) : 0.;
const auto percent = int(base::SafeRound(progress * 100));
if (_loadingPercent != percent) {
_loadingPercent = percent;
_downloadProgress->setText(QString::number(percent) + '%');
updateDownloadProgressPosition();
refreshFadeCache();
}
} else {
_downloadProgress.destroy();
}
}
void PlaybackControls::refreshFadeCache() {
if (!_fadeAnimation->animating()) {
return;
}
startFading([&] {
_fadeAnimation->refreshCache();
});
}
void PlaybackControls::updatePlayPauseResumeState(const Player::TrackState &state) {
auto showPause = ShowPauseIcon(state.state) || (_seekPositionMs >= 0);
if (showPause != _showPause) {
_showPause = showPause;
_playPauseResume->setIconOverride(_showPause ? &st::mediaviewPauseIcon : nullptr, _showPause ? &st::mediaviewPauseIconOver : nullptr);
}
}
void PlaybackControls::updateTimeTexts(const Player::TrackState &state) {
qint64 position = 0;
if (Player::IsStoppedAtEnd(state.state)) {
position = state.length;
} else if (!Player::IsStoppedOrStopping(state.state)) {
position = state.position;
} else {
position = 0;
}
auto playFrequency = state.frequency;
auto playAlready = position / playFrequency;
auto playLeft = (state.length / playFrequency) - playAlready;
_lastDurationMs = (state.length * crl::time(1000)) / playFrequency;
_timeAlready = Ui::FormatDurationText(playAlready);
auto minus = QChar(8722);
_timeLeft = minus + Ui::FormatDurationText(playLeft);
if (_seekPositionMs < 0) {
refreshTimeTexts();
}
}
void PlaybackControls::refreshTimeTexts() {
auto alreadyChanged = false, leftChanged = false;
auto timeAlready = _timeAlready;
auto timeLeft = _timeLeft;
if (_seekPositionMs >= 0) {
auto playAlready = _seekPositionMs / crl::time(1000);
auto playLeft = (_lastDurationMs / crl::time(1000)) - playAlready;
timeAlready = Ui::FormatDurationText(playAlready);
auto minus = QChar(8722);
timeLeft = minus + Ui::FormatDurationText(playLeft);
}
_playedAlready->setText(timeAlready, &alreadyChanged);
_toPlayLeft->setText(timeLeft, &leftChanged);
if (alreadyChanged || leftChanged) {
resizeEvent(nullptr);
refreshFadeCache();
}
}
void PlaybackControls::setInFullScreen(bool inFullScreen) {
if (_inFullScreen != inFullScreen) {
_inFullScreen = inFullScreen;
_fullScreenToggle->setIconOverride(
_inFullScreen ? &st::mediaviewFullScreenOutIcon : nullptr,
_inFullScreen ? &st::mediaviewFullScreenOutIconOver : nullptr);
}
}
void PlaybackControls::resizeEvent(QResizeEvent *e) {
const auto textSkip = st::mediaviewPlayProgressSkip;
const auto textLeft = st::mediaviewPlayProgressLeft;
const auto textTop = st::mediaviewPlayProgressTop;
_playedAlready->moveToLeft(textLeft + textSkip, textTop);
_toPlayLeft->moveToRight(textLeft + textSkip, textTop);
const auto remove = 2 * textLeft + 4 * textSkip + _playedAlready->width() + _toPlayLeft->width();
auto playbackWidth = width() - remove;
_playbackSlider->resize(playbackWidth, st::mediaviewPlayback.seekSize.height());
_playbackSlider->moveToLeft(textLeft + 2 * textSkip + _playedAlready->width(), st::mediaviewPlaybackTop);
_playPauseResume->moveToLeft(
(width() - _playPauseResume->width()) / 2,
st::mediaviewPlayButtonTop);
auto right = st::mediaviewButtonsRight;
if (_speedToggle) {
_speedToggle->moveToRight(right, st::mediaviewButtonsTop);
right += _speedToggle->width() + st::mediaviewPipButtonSkip;
}
_pictureInPicture->moveToRight(right, st::mediaviewButtonsTop);
right += _pictureInPicture->width() + st::mediaviewFullScreenButtonSkip;
_fullScreenToggle->moveToRight(right, st::mediaviewButtonsTop);
updateDownloadProgressPosition();
auto left = st::mediaviewVolumeToggleLeft;
_volumeToggle->moveToLeft(left, st::mediaviewVolumeTop);
left += _volumeToggle->width() + st::mediaviewVolumeSkip;
_volumeController->resize(
st::mediaviewVolumeWidth,
st::mediaviewPlayback.seekSize.height());
_volumeController->moveToLeft(left, st::mediaviewVolumeTop + (_volumeToggle->height() - _volumeController->height()) / 2);
}
void PlaybackControls::updateDownloadProgressPosition() {
if (!_downloadProgress) {
return;
}
const auto left = _playPauseResume->x() + _playPauseResume->width();
const auto right = _fullScreenToggle->x();
const auto available = right - left;
const auto x = left + (available - _downloadProgress->width()) / 2;
const auto y = _playPauseResume->y() + (_playPauseResume->height() - _downloadProgress->height()) / 2;
_downloadProgress->move(x, y);
}
void PlaybackControls::paintEvent(QPaintEvent *e) {
auto p = QPainter(this);
if (_fadeAnimation->paint(p)) {
return;
}
if (_childrenHidden) {
showChildren();
_playbackSlider->setFadeOpacity(1.);
_volumeController->setFadeOpacity(1.);
_childrenHidden = false;
}
Ui::FillRoundRect(p, rect(), st::mediaviewSaveMsgBg, Ui::MediaviewSaveCorners);
}
void PlaybackControls::mousePressEvent(QMouseEvent *e) {
e->accept(); // Don't pass event to the Media::View::OverlayWidget.
}
bool PlaybackControls::hasMenu() const {
return _speedController && _speedController->menu();
}
bool PlaybackControls::dragging() const {
return _volumeController->isChanging()
|| _playbackSlider->isChanging()
|| _playPauseResume->isOver()
|| _volumeToggle->isOver()
|| (_speedToggle && _speedToggle->isOver())
|| _fullScreenToggle->isOver()
|| _pictureInPicture->isOver()
|| hasMenu();
}
PlaybackControls::~PlaybackControls() = default;
} // namespace View
} // namespace Media

View File

@@ -0,0 +1,133 @@
/*
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 "base/object_ptr.h"
#include "media/media_common.h"
namespace Ui {
class LabelSimple;
class FadeAnimation;
class IconButton;
class MediaSlider;
class PopupMenu;
} // namespace Ui
namespace Media::Player {
struct TrackState;
class SettingsButton;
class SpeedController;
} // namespace Media::Player
namespace Media::View {
class PlaybackProgress;
class PlaybackControls : public Ui::RpWidget {
public:
class Delegate {
public:
virtual void playbackControlsPlay() = 0;
virtual void playbackControlsPause() = 0;
virtual void playbackControlsSeekProgress(crl::time position) = 0;
virtual void playbackControlsSeekFinished(crl::time position) = 0;
virtual void playbackControlsVolumeChanged(float64 volume) = 0;
[[nodiscard]] virtual float64 playbackControlsCurrentVolume() = 0;
virtual void playbackControlsVolumeToggled() = 0;
virtual void playbackControlsVolumeChangeFinished() = 0;
virtual void playbackControlsSpeedChanged(float64 speed) = 0;
[[nodiscard]] virtual float64 playbackControlsCurrentSpeed(
bool lastNonDefault) = 0;
[[nodiscard]] virtual auto playbackControlsQualities()
-> std::vector<int> = 0;
[[nodiscard]] virtual auto playbackControlsCurrentQuality()
-> VideoQuality = 0;
virtual void playbackControlsQualityChanged(int quality) = 0;
virtual void playbackControlsToFullScreen() = 0;
virtual void playbackControlsFromFullScreen() = 0;
virtual void playbackControlsToPictureInPicture() = 0;
virtual void playbackControlsRotate() = 0;
};
PlaybackControls(QWidget *parent, not_null<Delegate*> delegate);
~PlaybackControls();
void showAnimated();
void hideAnimated();
void updatePlayback(const Player::TrackState &state);
void setLoadingProgress(int64 ready, int64 total);
void setInFullScreen(bool inFullScreen);
[[nodiscard]] bool hasMenu() const;
[[nodiscard]] bool dragging() const;
protected:
void resizeEvent(QResizeEvent *e) override;
void paintEvent(QPaintEvent *e) override;
void mousePressEvent(QMouseEvent *e) override;
private:
void handleSeekProgress(float64 progress);
void handleSeekFinished(float64 progress);
template <typename Callback>
void startFading(Callback start);
void fadeFinished();
void fadeUpdated(float64 opacity);
void refreshFadeCache();
[[nodiscard]] float64 countDownloadedTillPercent(
const Player::TrackState &state) const;
void updatePlaybackSpeed(float64 speed);
void updateVolumeToggleIcon();
void updateDownloadProgressPosition();
void updatePlayPauseResumeState(const Player::TrackState &state);
void updateTimeTexts(const Player::TrackState &state);
void refreshTimeTexts();
[[nodiscard]] float64 speedLookup(bool lastNonDefault) const;
void saveSpeed(float64 speed);
void saveQuality(int quality);
void updateSpeedToggleQuality();
const not_null<Delegate*> _delegate;
bool _speedControllable = false;
std::vector<int> _qualitiesList;
bool _inFullScreen = false;
bool _showPause = false;
bool _childrenHidden = false;
QString _timeAlready, _timeLeft;
crl::time _seekPositionMs = -1;
crl::time _lastDurationMs = 0;
int64 _loadingReady = 0;
int64 _loadingTotal = 0;
int _loadingPercent = 0;
object_ptr<Ui::IconButton> _playPauseResume;
object_ptr<Ui::MediaSlider> _playbackSlider;
std::unique_ptr<PlaybackProgress> _playbackProgress;
std::unique_ptr<PlaybackProgress> _receivedTillProgress;
object_ptr<Ui::IconButton> _volumeToggle;
object_ptr<Ui::MediaSlider> _volumeController;
object_ptr<Player::SettingsButton> _speedToggle;
object_ptr<Ui::IconButton> _fullScreenToggle;
object_ptr<Ui::IconButton> _pictureInPicture;
object_ptr<Ui::LabelSimple> _playedAlready;
object_ptr<Ui::LabelSimple> _toPlayLeft;
object_ptr<Ui::LabelSimple> _downloadProgress = { nullptr };
std::unique_ptr<Player::SpeedController> _speedController;
std::unique_ptr<Ui::FadeAnimation> _fadeAnimation;
};
} // namespace Media::View

View File

@@ -0,0 +1,168 @@
/*
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 "media/view/media_view_playback_progress.h"
#include "media/audio/media_audio.h"
#include "styles/style_media_view.h"
namespace Media {
namespace View {
namespace {
constexpr auto kPlaybackAnimationDurationMs = crl::time(200);
} // namespace
PlaybackProgress::PlaybackProgress()
: _valueAnimation([=](crl::time now) {
return valueAnimationCallback(now);
})
, _availableTillAnimation([=](crl::time now) {
return availableTillAnimationCallback(now);
}) {
}
void PlaybackProgress::updateState(
const Player::TrackState &state,
float64 loadedTillPercent) {
_playing = !Player::IsStopped(state.state);
const auto length = state.length;
const auto position = Player::IsStoppedAtEnd(state.state)
? state.length
: Player::IsStoppedOrStopping(state.state)
? 0
: state.position;
const auto receivedTill = (length && state.receivedTill > position)
? state.receivedTill
: -1;
const auto loadedTill = (loadedTillPercent != 0.)
? int64(std::floor(loadedTillPercent * length))
: -1;
const auto availableTill = (length && loadedTill > position)
? std::max(receivedTill, loadedTill)
: receivedTill;
const auto wasInLoadingState = _inLoadingState;
if (wasInLoadingState) {
_inLoadingState = false;
if (_inLoadingStateChanged) {
_inLoadingStateChanged(false);
}
}
const auto progress = (position > length)
? 1.
: length
? std::clamp(float64(position) / length, 0., 1.)
: 0.;
const auto availableTillProgress = (availableTill > position)
? std::clamp(float64(availableTill) / length, 0., 1.)
: -1.;
const auto animatedPosition = position + (state.frequency * kPlaybackAnimationDurationMs / 1000);
const auto animatedProgress = length ? qMax(float64(animatedPosition) / length, 0.) : 0.;
if (length != _length || position != _position || wasInLoadingState) {
const auto animated = length
&& _length
&& (animatedProgress > value())
&& (position > _position)
&& (position < _position + state.frequency);
if (animated) {
setValue(animatedProgress, animated);
} else {
setValue(progress, animated);
}
_position = position;
_length = length;
}
if (availableTill != _availableTill) {
setAvailableTill(availableTillProgress);
_availableTill = availableTill;
}
}
void PlaybackProgress::updateLoadingState(float64 progress) {
if (!_inLoadingState) {
_inLoadingState = true;
if (_inLoadingStateChanged) {
_inLoadingStateChanged(true);
}
}
auto animated = (progress > value());
setValue(progress, animated);
}
float64 PlaybackProgress::value() const {
return qMin(a_value.current(), 1.);
}
void PlaybackProgress::setValue(float64 value, bool animated) {
if (animated) {
valueAnimationCallback(crl::now());
a_value.start(value);
_valueAnimation.start();
} else {
a_value = anim::value(value, value);
_valueAnimation.stop();
}
emitUpdatedValue();
}
void PlaybackProgress::setAvailableTill(float64 value) {
const auto current = a_availableTill.current();
if (value > current && current > 0.) {
availableTillAnimationCallback(crl::now());
a_availableTill.start(value);
_availableTillAnimation.start();
} else if (value > a_value.current()) {
a_availableTill = anim::value(a_value.current(), value);
_availableTillAnimation.start();
} else {
a_availableTill = anim::value(-1., -1.);
_availableTillAnimation.stop();
}
emitUpdatedValue();
}
bool PlaybackProgress::valueAnimationCallback(float64 now) {
const auto time = (now - _valueAnimation.started());
const auto dt = anim::Disabled()
? 1.
: (time / kPlaybackAnimationDurationMs);
if (dt >= 1.) {
a_value.finish();
} else {
a_value.update(dt, anim::linear);
}
emitUpdatedValue();
return (dt < 1.);
}
bool PlaybackProgress::availableTillAnimationCallback(float64 now) {
const auto time = now - _availableTillAnimation.started();
const auto dt = anim::Disabled()
? 1.
: (time / kPlaybackAnimationDurationMs);
if (dt >= 1.) {
a_availableTill.finish();
} else {
a_availableTill.update(dt, anim::linear);
}
emitUpdatedValue();
return (dt < 1.);
}
void PlaybackProgress::emitUpdatedValue() {
if (_valueChanged) {
const auto value = a_value.current();
const auto availableTill = a_availableTill.current();
_valueChanged(value, std::max(value, availableTill));
}
}
} // namespace View
} // namespace Media

View File

@@ -0,0 +1,62 @@
/*
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/widgets/continuous_sliders.h"
namespace Media {
namespace Player {
struct TrackState;
} // namespace Player
namespace View {
class PlaybackProgress {
public:
PlaybackProgress();
void setValueChangedCallback(Fn<void(float64,float64)> callback) {
_valueChanged = std::move(callback);
}
void setInLoadingStateChangedCallback(Fn<void(bool)> callback) {
_inLoadingStateChanged = std::move(callback);
}
void setValue(float64 value, bool animated);
[[nodiscard]] float64 value() const;
void updateState(
const Player::TrackState &state,
float64 loadedTillPercent = 0.);
void updateLoadingState(float64 progress);
private:
bool valueAnimationCallback(float64 now);
bool availableTillAnimationCallback(float64 now);
void setAvailableTill(float64 value);
void emitUpdatedValue();
// This can animate for a very long time (like in music playing),
// so it should be a Basic, not a Simple animation, because
// Simple-s pauses mtproto responses/updates handling while playing.
anim::value a_value, a_availableTill;
Ui::Animations::Basic _valueAnimation, _availableTillAnimation;
Fn<void(float64,float64)> _valueChanged;
bool _inLoadingState = false;
Fn<void(bool)> _inLoadingStateChanged;
int64 _position = 0;
int64 _length = 0;
int64 _availableTill = -1;
bool _playing = false;
};
} // namespace View
} // namespace Media

View File

@@ -0,0 +1,767 @@
/*
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 "media/view/media_view_playback_sponsored.h"
#include "boxes/premium_preview_box.h"
#include "data/components/sponsored_messages.h"
#include "data/data_file_origin.h"
#include "data/data_photo.h"
#include "data/data_photo_media.h"
#include "data/data_session.h"
#include "history/history.h"
#include "history/history_item.h"
#include "lang/lang_keys.h"
#include "main/main_session.h"
#include "menu/menu_sponsored.h"
#include "ui/effects/numbers_animation.h"
#include "ui/effects/ripple_animation.h"
#include "ui/widgets/menu/menu_add_action_callback.h"
#include "ui/widgets/menu/menu_add_action_callback_factory.h"
#include "ui/widgets/buttons.h"
#include "ui/widgets/popup_menu.h"
#include "ui/basic_click_handlers.h"
#include "ui/painter.h"
#include "ui/ui_utility.h"
#include "ui/cached_round_corners.h"
#include "styles/style_chat.h"
#include "styles/style_media_view.h"
namespace Media::View {
namespace {
constexpr auto kStartDelayMin = crl::time(1000);
constexpr auto kDurationMin = 5 * crl::time(1000);
enum class Action {
Close,
PromotePremium,
Pause,
Unpause,
};
class Close final : public Ui::RippleButton {
public:
Close(
not_null<QWidget*> parent,
const style::RippleAnimation &st,
rpl::producer<crl::time> allowCloseAt);
[[nodiscard]] rpl::producer<Action> actions() const;
private:
QPoint prepareRippleStartPosition() const override;
QImage prepareRippleMask() const override;
void paintEvent(QPaintEvent *e) override;
void updateProgress(crl::time now);
rpl::event_stream<Action> _actions;
Ui::NumbersAnimation _countdown;
Ui::Animations::Basic _progress;
base::Timer _noAnimationTimer;
crl::time _allowCloseAt = 0;
crl::time _startedAt = 0;
crl::time _pausedAt = 0;
int _secondsTill = 0;
int _rippleSize = 0;
QPoint _rippleOrigin;
bool _allowClose = false;
};
Close::Close(
not_null<QWidget*> parent,
const style::RippleAnimation &st,
rpl::producer<crl::time> allowCloseAt)
: RippleButton(parent, st)
, _countdown(st::mediaSponsoredCloseFont, [=] { update(); })
, _progress([=](crl::time now) { updateProgress(now); })
, _noAnimationTimer([=] { updateProgress(crl::now()); })
, _startedAt(crl::now()) {
resize(st::mediaSponsoredCloseFull, st::mediaSponsoredCloseFull);
const auto size = st::mediaSponsoredCloseRipple;
const auto cut = int(base::SafeRound((width() - size) / 2.));
_rippleSize = std::min(width() - 2 * cut, height() - 2 * cut);
_rippleOrigin = QPoint(
(width() - _rippleSize) / 2,
(height() - _rippleSize) / 2);
std::move(
allowCloseAt
) | rpl::on_next([=](crl::time at) {
const auto now = crl::now();
if (!at) {
updateProgress(now);
_pausedAt = now;
_progress.stop();
} else {
if (_pausedAt) {
_startedAt += now - base::take(_pausedAt);
}
_allowCloseAt = at;
updateProgress(now);
if (!anim::Disabled()) {
_progress.start();
} else if (!_allowClose) {
_noAnimationTimer.callEach(crl::time(200));
}
}
}, lifetime());
updateProgress(_startedAt);
setClickedCallback([=] {
_actions.fire(_allowClose ? Action::Close : Action::PromotePremium);
});
}
rpl::producer<Action> Close::actions() const {
return _actions.events();
}
void Close::updateProgress(crl::time now) {
update();
}
QPoint Close::prepareRippleStartPosition() const {
return mapFromGlobal(QCursor::pos()) - _rippleOrigin;
}
QImage Close::prepareRippleMask() const {
return Ui::RippleAnimation::EllipseMask({ _rippleSize, _rippleSize });
}
void Close::paintEvent(QPaintEvent *e) {
auto p = QPainter(this);
paintRipple(p, _rippleOrigin);
const auto now = crl::now();
if (!_pausedAt) {
_allowClose = (now >= _allowCloseAt);
}
const auto msTill = _allowCloseAt - (_pausedAt ? _pausedAt : now);
const auto msFull = _allowCloseAt - _startedAt;
const auto secondsTill = (std::max(msTill, crl::time()) + 999) / 1000;
const auto secondsFull = (std::max(msFull, crl::time()) + 999) / 1000;
const auto allowCloseLeft = anim::Disabled()
? (secondsFull ? (secondsTill / float64(secondsFull)) : 0)
: std::max(msFull ? (msTill / float64(msFull)) : 0., 0.);
const auto duration = crl::time(st::fadeWrapDuration);
const auto allowedProgress = anim::Disabled()
? (secondsTill ? 0. : 1.)
: std::clamp(-msTill, crl::time(), duration) / float64(duration);
if (_secondsTill != secondsTill) {
const auto initial = !_secondsTill;
_secondsTill = secondsTill;
_countdown.setText(QString::number(_secondsTill), _secondsTill);
if (initial) {
_countdown.finishAnimating();
}
}
auto pen = st::mediaviewTextLinkFg->p;
if (allowedProgress < 1.) {
if (allowedProgress > 0.) {
p.setOpacity(1. - allowedProgress);
}
p.setPen(pen);
const auto inner = QRect(
(width() - st::mediaSponsoredCloseDiameter) / 2,
(height() - st::mediaSponsoredCloseDiameter) / 2,
st::mediaSponsoredCloseDiameter,
st::mediaSponsoredCloseDiameter);
p.setFont(st::mediaSponsoredCloseFont);
_countdown.paint(
p,
inner.x() + (inner.width() - _countdown.countWidth()) / 2,
(inner.y()
+ (inner.height()
- st::mediaSponsoredCloseFont->height) / 2),
width());
const auto skip = 0.23;
const auto len = int(base::SafeRound(
arc::kFullLength * (1. - skip) * allowCloseLeft));
if (len > 0) {
const auto from = arc::kFullLength / 4;
auto hq = PainterHighQualityEnabler(p);
pen.setWidthF(st::mediaSponsoredCloseStroke);
pen.setCapStyle(Qt::RoundCap);
p.setPen(pen);
p.drawArc(inner, from, len);
}
p.setOpacity(1.);
}
const auto sizeFinal = st::mediaSponsoredCloseSize;
const auto sizeSmall = st::mediaSponsoredCloseCorner;
const auto twiceFinal = st::mediaSponsoredCloseTwice;
const auto twiceSmall = st::mediaSponsoredCloseSmall;
const auto size = sizeSmall + allowedProgress * (sizeFinal - sizeSmall);
const auto twice = twiceSmall
+ allowedProgress * (twiceFinal - twiceSmall);
const auto leftFinal = (width() - size) / 2.;
const auto leftSmall = (width() + st::mediaSponsoredCloseDiameter) / 2.
- (st::mediaSponsoredCloseStroke / 2.)
- sizeSmall;
const auto topFinal = (height() - size) / 2.;
const auto topSmall = (height() - st::mediaSponsoredCloseDiameter) / 2.;
const auto left = leftSmall + allowedProgress * (leftFinal - leftSmall);
const auto top = topSmall + allowedProgress * (topFinal - topSmall);
auto hq = PainterHighQualityEnabler(p);
pen.setWidthF(twice / 2.);
p.setPen(pen);
p.drawLine(QPointF(left, top), QPointF(left + size, top + size));
p.drawLine(QPointF(left + size, top), QPointF(left, top + size));
}
[[nodiscard]] style::RoundButton PrepareAboutStyle() {
static auto textBg = style::complex_color([] {
auto result = st::mediaviewTextLinkFg->c;
result.setAlphaF(result.alphaF() * 0.1);
return result;
});
static auto textBgOver = style::complex_color([] {
auto result = st::mediaviewTextLinkFg->c;
result.setAlphaF(result.alphaF() * 0.15);
return result;
});
static auto rippleColor = style::complex_color([] {
auto result = st::mediaviewTextLinkFg->c;
result.setAlphaF(result.alphaF() * 0.2);
return result;
});
auto result = st::mediaSponsoredAbout;
result.textFg = st::mediaviewTextLinkFg;
result.textFgOver = st::mediaviewTextLinkFg;
result.textBg = textBg.color();
result.textBgOver = textBgOver.color();
result.ripple.color = rippleColor.color();
return result;
}
} // namespace
class PlaybackSponsored::Message final : public Ui::RpWidget {
public:
Message(
QWidget *parent,
std::shared_ptr<ChatHelpers::Show> show,
const Data::SponsoredMessage &data,
rpl::producer<crl::time> allowCloseAt);
[[nodiscard]] rpl::producer<Action> actions() const;
void setFinalPosition(int x, int y);
void fadeIn();
void fadeOut(Fn<void()> hidden);
private:
void paintEvent(QPaintEvent *e) override;
void mouseMoveEvent(QMouseEvent *e) override;
void mousePressEvent(QMouseEvent *e) override;
void mouseReleaseEvent(QMouseEvent *e) override;
int resizeGetHeight(int newWidth) override;
void populate();
void startFadeIn();
void updateShown(Fn<void()> finished = nullptr);
void startFade(Fn<void()> finished);
const not_null<Main::Session*> _session;
const std::shared_ptr<ChatHelpers::Show> _show;
const Data::SponsoredMessage _data;
style::RoundButton _aboutSt;
std::unique_ptr<Ui::RoundButton> _about;
std::unique_ptr<Close> _close;
base::unique_qptr<Ui::PopupMenu> _menu;
rpl::event_stream<Action> _actions;
std::shared_ptr<Data::PhotoMedia> _photo;
Ui::Text::String _title;
Ui::Text::String _text;
QPoint _finalPosition;
int _left = 0;
int _top = 0;
int _titleHeight = 0;
int _textHeight = 0;
QImage _cache;
Ui::Animations::Simple _showAnimation;
bool _shown = false;
bool _over = false;
bool _pressed = false;
rpl::lifetime _photoLifetime;
};
PlaybackSponsored::Message::Message(
QWidget *parent,
std::shared_ptr<ChatHelpers::Show> show,
const Data::SponsoredMessage &data,
rpl::producer<crl::time> allowCloseAt)
: RpWidget(parent)
, _session(&data.history->session())
, _show(std::move(show))
, _data(data)
, _aboutSt(PrepareAboutStyle())
, _about(std::make_unique<Ui::RoundButton>(
this,
tr::lng_search_sponsored_button(),
_aboutSt))
, _close(
std::make_unique<Close>(
this,
_aboutSt.ripple,
std::move(allowCloseAt))) {
_about->setTextTransform(Ui::RoundButton::TextTransform::NoTransform);
setMouseTracking(true);
populate();
hide();
}
rpl::producer<Action> PlaybackSponsored::Message::actions() const {
return rpl::merge(_actions.events(), _close->actions());
}
void PlaybackSponsored::Message::setFinalPosition(int x, int y) {
_finalPosition = { x, y };
if (_shown) {
updateShown();
}
}
void PlaybackSponsored::Message::fadeIn() {
_shown = true;
if (!_photo || _photo->loaded()) {
startFadeIn();
return;
}
_photo->owner()->session().downloaderTaskFinished(
) | rpl::filter([=] {
return _photo->loaded();
}) | rpl::on_next([=] {
_photoLifetime.destroy();
startFadeIn();
}, _photoLifetime);
}
void PlaybackSponsored::Message::startFadeIn() {
if (!_shown) {
return;
}
startFade([=] {
_session->sponsoredMessages().view(_data.randomId);
});
show();
}
void PlaybackSponsored::Message::fadeOut(Fn<void()> hidden) {
if (!_shown) {
if (const auto onstack = hidden) {
onstack();
}
return;
}
_shown = false;
startFade(std::move(hidden));
}
void PlaybackSponsored::Message::startFade(Fn<void()> finished) {
_cache = Ui::GrabWidgetToImage(this);
_about->hide();
_close->hide();
const auto from = _shown ? 0. : 1.;
const auto till = _shown ? 1. : 0.;
_showAnimation.start([=] {
updateShown(finished);
}, from, till, st::fadeWrapDuration);
}
void PlaybackSponsored::Message::updateShown(Fn<void()> finished) {
const auto shown = _showAnimation.value(_shown ? 1. : 0.);
const auto shift = anim::interpolate(st::mediaSponsoredShift, 0, shown);
move(_finalPosition.x(), _finalPosition.y() + shift);
update();
if (!_showAnimation.animating()) {
_cache = QImage();
_close->show();
_about->show();
if (const auto onstack = finished) {
onstack();
}
}
}
void PlaybackSponsored::Message::paintEvent(QPaintEvent *e) {
auto p = QPainter(this);
const auto shown = _showAnimation.value(_shown ? 1. : 0.);
if (!_cache.isNull()) {
p.setOpacity(shown);
p.drawImage(0, 0, _cache);
return;
}
Ui::FillRoundRect(
p,
rect(),
st::mediaviewSaveMsgBg,
Ui::MediaviewSaveCorners);
const auto &padding = st::mediaSponsoredPadding;
if (_photo) {
if (const auto image = _photo->image(Data::PhotoSize::Large)) {
const auto size = st::mediaSponsoredThumb;
const auto x = padding.left();
const auto y = (height() - size) / 2;
p.drawPixmap(
x,
y,
image->pixSingle(
size,
size,
{ .options = Images::Option::RoundCircle }));
}
}
p.setPen(st::mediaviewControlFg);
_title.draw(p, {
.position = { _left, _top },
.availableWidth = _about->x() - _left,
.palette = &st::mediaviewTextPalette,
});
_text.draw(p, {
.position = { _left, _top + _titleHeight },
.availableWidth = _close->x() - _left,
.palette = &st::mediaviewTextPalette,
});
}
void PlaybackSponsored::Message::mouseMoveEvent(QMouseEvent *e) {
const auto &padding = st::mediaSponsoredPadding;
const auto point = e->pos();
const auto about = _about->geometry();
const auto close = _close->geometry();
const auto over = !about.marginsAdded(padding).contains(point)
&& !close.marginsAdded(padding).contains(point);
if (_over != over) {
_over = over;
setCursor(_over ? style::cur_pointer : style::cur_default);
}
}
void PlaybackSponsored::Message::mousePressEvent(QMouseEvent *e) {
if (_over) {
_pressed = true;
}
}
void PlaybackSponsored::Message::mouseReleaseEvent(QMouseEvent *e) {
if (base::take(_pressed) && _over) {
_session->sponsoredMessages().clicked(_data.randomId, false, false);
UrlClickHandler::Open(_data.link);
}
}
int PlaybackSponsored::Message::resizeGetHeight(int newWidth) {
const auto &padding = st::mediaSponsoredPadding;
const auto userpic = st::mediaSponsoredThumb;
const auto innerWidth = newWidth - _left - _close->width();
const auto titleWidth = innerWidth - _about->width() - padding.right();
_titleHeight = _title.countHeight(titleWidth);
_textHeight = _text.countHeight(innerWidth);
const auto use = std::max(_titleHeight + _textHeight, userpic);
const auto height = padding.top() + use + padding.bottom();
_left = padding.left() + (_photo ? (userpic + padding.left()) : 0);
_top = padding.top() + (use - _titleHeight - _textHeight) / 2;
_about->move(
_left + std::min(titleWidth, _title.maxWidth()) + padding.right(),
_top);
_close->move(
newWidth - _close->width(),
(height - _close->height()) / 2);
return height;
}
void PlaybackSponsored::Message::populate() {
const auto &from = _data.from;
const auto photo = from.photoId
? _data.history->owner().photo(from.photoId).get()
: nullptr;
if (photo) {
_photo = photo->createMediaView();
photo->load({}, LoadFromCloudOrLocal, true);
}
_title = Ui::Text::String(
st::semiboldTextStyle,
from.title,
kDefaultTextOptions,
st::msgMinWidth);
_text = Ui::Text::String(
st::defaultTextStyle,
_data.textWithEntities,
kMarkupTextOptions,
st::msgMinWidth);
_about->setClickedCallback([=] {
_menu = nullptr;
const auto parent = parentWidget();
_menu = base::make_unique_q<Ui::PopupMenu>(
parent,
st::mediaviewPopupMenu);
const auto raw = _menu.get();
const auto addAction = Ui::Menu::CreateAddActionCallback(raw);
Menu::FillSponsored(
addAction,
_show,
Menu::SponsoredPhrases::Channel,
_session->sponsoredMessages().lookupDetails(_data),
_session->sponsoredMessages().createReportCallback(
_data.randomId,
crl::guard(this, [=] { _actions.fire(Action::Close); })),
{ .dark = true });
_actions.fire(Action::Pause);
Ui::Connect(raw, &QObject::destroyed, this, [=] {
_actions.fire(Action::Unpause);
});
raw->popup(QCursor::pos());
});
}
PlaybackSponsored::PlaybackSponsored(
not_null<Ui::RpWidget*> controls,
std::shared_ptr<ChatHelpers::Show> show,
not_null<HistoryItem*> item)
: _parent(controls->parentWidget())
, _session(&item->history()->session())
, _show(std::move(show))
, _itemId(item->fullId())
, _controlsGeometry(controls->geometryValue())
, _timer([=] { update(); }) {
_session->sponsoredMessages().requestForVideo(item, crl::guard(this, [=](
Data::SponsoredForVideo data) {
if (data.list.empty()) {
return;
}
_data = std::move(data);
if (_data->state.initial()
|| (_data->state.itemIndex > _data->list.size())
|| (_data->state.itemIndex == _data->list.size()
&& _data->state.leftTillShow <= 0)) {
_data->state.itemIndex = 0;
_data->state.leftTillShow = std::max(
_data->startDelay,
kStartDelayMin);
}
update();
}));
}
PlaybackSponsored::~PlaybackSponsored() {
saveState();
}
void PlaybackSponsored::start() {
_started = true;
if (!_paused) {
_start = crl::now();
update();
}
}
void PlaybackSponsored::setPaused(bool paused) {
setPausedOutside(paused);
}
void PlaybackSponsored::updatePaused() {
const auto paused = _pausedInside || _pausedOutside;
if (_paused == paused) {
return;
} else if (_started && paused) {
update();
}
_paused = paused;
if (!_started) {
return;
} else if (_paused) {
_start = 0;
_timer.cancel();
_allowCloseAt = 0;
} else {
_start = crl::now();
update();
}
}
void PlaybackSponsored::setPausedInside(bool paused) {
if (_pausedInside == paused) {
return;
}
_pausedInside = paused;
updatePaused();
}
void PlaybackSponsored::setPausedOutside(bool paused) {
if (_pausedOutside == paused) {
return;
}
_pausedOutside = paused;
updatePaused();
}
void PlaybackSponsored::finish() {
_timer.cancel();
if (_data) {
saveState();
_data = std::nullopt;
}
}
void PlaybackSponsored::update() {
if (!_data || !_start) {
return;
}
const auto [now, state] = computeState();
const auto message = (_data->state.itemIndex < _data->list.size())
? &_data->list[state.itemIndex]
: nullptr;
const auto duration = message
? std::max(
message->durationMin + kDurationMin,
message->durationMax)
: crl::time(0);
if (_data->state.leftTillShow > 0 && state.leftTillShow <= 0) {
_data->state.leftTillShow = 0;
if (duration) {
_allowCloseAt = now + message->durationMin;
show(*message);
_start = now;
_timer.callOnce(duration);
saveState();
} else {
finish();
}
} else if (_data->state.leftTillShow <= 0
&& state.leftTillShow <= -duration) {
hide(now);
} else {
if (state.leftTillShow <= 0 && duration) {
_allowCloseAt = now + state.leftTillShow + message->durationMin;
if (!_widget) {
show(*message);
}
}
_data->state = state;
_timer.callOnce((state.leftTillShow > 0)
? state.leftTillShow
: (state.leftTillShow + duration));
}
}
void PlaybackSponsored::show(const Data::SponsoredMessage &data) {
_widget = std::make_unique<Message>(
_parent,
_show,
data,
_allowCloseAt.value());
const auto raw = _widget.get();
_controlsGeometry.value() | rpl::on_next([=](QRect controls) {
raw->resizeToWidth(controls.width());
raw->setFinalPosition(
controls.x(),
controls.y() - st::mediaSponsoredSkip - raw->height());
}, raw->lifetime());
raw->actions() | rpl::on_next([=](Action action) {
switch (action) {
case Action::Close: hide(crl::now()); break;
case Action::PromotePremium: showPremiumPromo(); break;
case Action::Pause: setPausedInside(true); break;
case Action::Unpause: setPausedInside(false); break;
}
}, raw->lifetime());
raw->fadeIn();
}
void PlaybackSponsored::showPremiumPromo() {
ShowPremiumPreviewBox(_show, PremiumFeature::NoAds);
}
void PlaybackSponsored::hide(crl::time now) {
Expects(_widget != nullptr);
_widget->fadeOut([this, raw = _widget.get()] {
if (_widget.get() == raw) {
_widget = nullptr;
}
});
++_data->state.itemIndex;
_data->state.leftTillShow = std::max(
_data->betweenDelay,
kStartDelayMin);
_start = now;
_timer.callOnce(_data->state.leftTillShow);
saveState();
}
void PlaybackSponsored::saveState() {
_session->sponsoredMessages().updateForVideo(
_itemId,
computeState().data);
}
PlaybackSponsored::State PlaybackSponsored::computeState() const {
auto result = State{ crl::now() };
if (!_data) {
return result;
}
result.data = _data->state;
if (!_start) {
return result;
}
const auto elapsed = result.now - _start;
result.data.leftTillShow -= elapsed;
return result;
}
rpl::lifetime &PlaybackSponsored::lifetime() {
return _lifetime;
}
bool PlaybackSponsored::Has(HistoryItem *item) {
return item
&& item->history()->session().sponsoredMessages().canHaveFor(item);
}
} // namespace Media::View

View File

@@ -0,0 +1,83 @@
/*
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/timer.h"
#include "base/weak_ptr.h"
#include "data/components/sponsored_messages.h"
namespace ChatHelpers {
class Show;
} // namespace ChatHelpers
namespace Main {
class Session;
} // namespace Main
namespace Ui {
class RpWidget;
} // namespace Ui
namespace Media::View {
class PlaybackSponsored final : public base::has_weak_ptr {
public:
PlaybackSponsored(
not_null<Ui::RpWidget*> controls,
std::shared_ptr<ChatHelpers::Show> show,
not_null<HistoryItem*> item);
~PlaybackSponsored();
void start();
void setPaused(bool paused);
[[nodiscard]] rpl::lifetime &lifetime();
[[nodiscard]] static bool Has(HistoryItem *item);
private:
class Message;
struct State {
crl::time now = 0;
Data::SponsoredForVideoState data;
};
void update();
void finish();
void updatePaused();
void showPremiumPromo();
void setPausedInside(bool paused);
void setPausedOutside(bool paused);
void show(const Data::SponsoredMessage &data);
void hide(crl::time now);
[[nodiscard]] State computeState() const;
void saveState();
const not_null<QWidget*> _parent;
const not_null<Main::Session*> _session;
const std::shared_ptr<ChatHelpers::Show> _show;
const FullMsgId _itemId;
rpl::variable<QRect> _controlsGeometry;
std::unique_ptr<Message> _widget;
rpl::variable<crl::time> _allowCloseAt;
crl::time _start = 0;
bool _started = false;
bool _paused = false;
bool _pausedInside = false;
bool _pausedOutside = false;
base::Timer _timer;
std::optional<Data::SponsoredForVideo> _data;
rpl::lifetime _lifetime;
};
} // namespace Media::View

View File

@@ -0,0 +1,379 @@
/*
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 "media/view/media_view_video_stream.h"
#include "data/data_message_reactions.h"
#include "calls/group/calls_group_call.h"
#include "calls/group/calls_group_common.h"
#include "calls/group/calls_group_members.h"
#include "calls/group/calls_group_messages.h"
#include "calls/group/calls_group_messages_ui.h"
#include "calls/group/calls_group_viewport.h"
#include "calls/calls_instance.h"
#include "chat_helpers/compose/compose_show.h"
#include "core/application.h"
#include "core/core_settings.h"
#include "payments/ui/payments_reaction_box.h"
#include "ui/effects/path_shift_gradient.h"
#include "ui/painter.h"
#include "styles/style_calls.h"
#include "styles/style_media_view.h"
namespace Media::View {
class VideoStream::Delegate final : public Calls::GroupCall::Delegate {
public:
explicit Delegate(Fn<void()> close);
void groupCallFinished(not_null<Calls::GroupCall*> call) override;
void groupCallFailed(not_null<Calls::GroupCall*> call) override;
void groupCallRequestPermissionsOrFail(Fn<void()> onSuccess) override;
void groupCallPlaySound(GroupCallSound sound) override;
auto groupCallGetVideoCapture(const QString &deviceId)
-> std::shared_ptr<tgcalls::VideoCaptureInterface> override;
FnMut<void()> groupCallAddAsyncWaiter() override;
private:
Fn<void()> _close;
};
class VideoStream::Loading final {
public:
Loading(not_null<QWidget*> parent, not_null<VideoStream*> stream);
void lower();
void setGeometry(int x, int y, int width, int height);
[[nodiscard]] rpl::lifetime &lifetime();
private:
void setup(not_null<QWidget*> parent);
const not_null<VideoStream*> _stream;
std::unique_ptr<Ui::RpWidget> _bg;
std::unique_ptr<Ui::PathShiftGradient> _gradient;
};
auto TopVideoStreamDonors(not_null<Calls::GroupCall*> call)
-> rpl::producer<std::vector<Data::MessageReactionsTopPaid>> {
const auto messages = call->messages();
return rpl::single(rpl::empty) | rpl::then(
messages->starsValueChanges() | rpl::to_empty
) | rpl::map([=] {
const auto &list = messages->starsTop().topDonors;
auto still = Ui::MaxTopPaidDonorsShown();
auto result = std::vector<Data::MessageReactionsTopPaid>();
result.reserve(list.size());
for (const auto &item : list) {
result.push_back({
.peer = item.peer,
.count = uint32(item.stars),
.my = item.my ? 1U : 0U,
});
if (!item.my && !--still) {
break;
}
}
return result;
});
}
auto TopDonorPlaces(not_null<Calls::GroupCall*> call)
-> rpl::producer<std::vector<not_null<PeerData*>>> {
return TopVideoStreamDonors(
call
) | rpl::map([=](const std::vector<Data::MessageReactionsTopPaid> &lst) {
auto result = std::vector<not_null<PeerData*>>();
auto left = Ui::MaxTopPaidDonorsShown();
result.reserve(lst.size());
for (const auto &donor : lst) {
result.push_back(donor.peer);
if (!--left) {
break;
}
}
return result;
});
}
VideoStream::Delegate::Delegate(Fn<void()> close)
: _close(std::move(close)) {
}
void VideoStream::Delegate::groupCallFinished(
not_null<Calls::GroupCall*> call) {
crl::on_main(call, _close);
}
void VideoStream::Delegate::groupCallFailed(not_null<Calls::GroupCall*> call) {
crl::on_main(call, _close);
}
void VideoStream::Delegate::groupCallRequestPermissionsOrFail(
Fn<void()> onSuccess) {
}
void VideoStream::Delegate::groupCallPlaySound(GroupCallSound sound) {
}
auto VideoStream::Delegate::groupCallGetVideoCapture(const QString &deviceId)
-> std::shared_ptr<tgcalls::VideoCaptureInterface> {
return nullptr;
}
FnMut<void()> VideoStream::Delegate::groupCallAddAsyncWaiter() {
return [] {};
}
VideoStream::Loading::Loading(
not_null<QWidget*> parent,
not_null<VideoStream*> stream)
: _stream(stream) {
setup(parent);
}
void VideoStream::Loading::lower() {
_bg->lower();
}
void VideoStream::Loading::setGeometry(int x, int y, int width, int height) {
_bg->setGeometry(x, y, width, height);
}
rpl::lifetime &VideoStream::Loading::lifetime() {
return _bg->lifetime();
}
void VideoStream::Loading::setup(not_null<QWidget*> parent) {
_bg = std::make_unique<Ui::RpWidget>(parent);
_gradient = std::make_unique<Ui::PathShiftGradient>(
st::storiesComposeBg,
st::storiesComposeBgRipple,
[=] { _bg->update(); });
_bg->show();
_bg->paintRequest() | rpl::on_next([=] {
auto p = QPainter(_bg.get());
auto hq = PainterHighQualityEnabler(p);
_gradient->startFrame(0, _bg->width(), _bg->width() / 3);
_gradient->paint([&](const Ui::PathShiftGradient::Background &bg) {
const auto stroke = style::ConvertScaleExact(2);
if (const auto color = std::get_if<style::color>(&bg)) {
auto pen = (*color)->p;
pen.setWidthF(stroke);
p.setPen(pen);
p.setBrush(*color);
} else {
const auto gradient = v::get<QLinearGradient*>(bg);
auto copy = *gradient;
copy.setStops({
{ 0., st::storiesComposeBg->c },
{ 0.5, st::white->c },
{ 1., st::storiesComposeBg->c },
});
auto pen = QPen(QBrush(copy), stroke);
p.setPen(pen);
p.setBrush(*gradient);
}
const auto half = stroke / 2.;
const auto remove = QMarginsF(half, half, half, half);
const auto rect = QRectF(_bg->rect()).marginsRemoved(remove);
const auto radius = st::storiesRadius - half;
p.drawRoundedRect(rect, radius, radius);
return true;
});
}, _bg->lifetime());
}
VideoStream::VideoStream(
not_null<QWidget*> parent,
not_null<Ui::RpWidgetWrap*> borrowedRp,
bool borrowedOpenGL,
Ui::GL::Backend backend,
std::shared_ptr<ChatHelpers::Show> show,
std::shared_ptr<Data::GroupCall> call,
QString callLinkSlug,
MsgId callJoinMessageId)
: _show(std::move(show))
, _delegate(std::make_unique<Delegate>([=] { _closeRequests.fire({}); }))
, _loading(std::make_unique<Loading>(parent, this))
, _call(std::make_unique<Calls::GroupCall>(
_delegate.get(),
Calls::StartConferenceInfo{
.show = _show,
.call = std::move(call),
.linkSlug = callLinkSlug,
.joinMessageId = callJoinMessageId,
}))
, _members(
std::make_unique<Calls::Group::Members>(
parent,
_call.get(),
Calls::Group::PanelMode::VideoStream,
backend))
, _viewport(
std::make_unique<Calls::Group::Viewport>(
parent,
Calls::Group::PanelMode::VideoStream,
backend,
borrowedRp,
borrowedOpenGL))
, _messages(
std::make_unique<Calls::Group::MessagesUi>(
parent,
_show,
Calls::Group::MessagesMode::VideoStream,
_call->messages()->listValue(),
TopDonorPlaces(_call.get()),
_call->messages()->idUpdates(),
_call->canManageValue(),
_commentsShown.value())) {
Core::App().calls().registerVideoStream(_call.get());
setupMembers();
setupVideo();
setupMessages();
}
VideoStream::~VideoStream() {
}
not_null<Calls::GroupCall*> VideoStream::call() const {
return _call.get();
}
rpl::producer<> VideoStream::closeRequests() const {
return _closeRequests.events();
}
void VideoStream::updateGeometry(int x, int y, int width, int height) {
const auto skip = st::groupCallMessageSkip;
_viewport->setGeometry(false, { x, y, width, height });
_messages->move(x + skip, y + height, width - 2 * skip, height / 2);
if (_loading) {
_loading->setGeometry(x, y, width, height);
}
}
void VideoStream::toggleCommentsOn(rpl::producer<bool> shown) {
_commentsShown = std::move(shown);
}
void VideoStream::ensureBorrowedRenderer(QOpenGLFunctions &f) {
_viewport->ensureBorrowedRenderer(f);
}
void VideoStream::borrowedPaint(QOpenGLFunctions &f) {
_viewport->borrowedPaint(f);
}
void VideoStream::ensureBorrowedRenderer() {
_viewport->ensureBorrowedRenderer();
}
void VideoStream::borrowedPaint(Painter &p, const QRegion &clip) {
_viewport->borrowedPaint(p, clip);
}
rpl::lifetime &VideoStream::lifetime() {
return _lifetime;
}
void VideoStream::setupMembers() {
}
void VideoStream::setupVideo() {
const auto setupTile = [=](
const Calls::VideoEndpoint &endpoint,
const std::unique_ptr<Calls::GroupCall::VideoTrack> &track) {
using namespace rpl::mappers;
const auto row = endpoint.rtmp()
? _members->rtmpFakeRow(Calls::GroupCall::TrackPeer(track)).get()
: _members->lookupRow(Calls::GroupCall::TrackPeer(track));
Assert(row != nullptr);
auto pinned = rpl::combine(
_call->videoEndpointLargeValue(),
_call->videoEndpointPinnedValue()
) | rpl::map(_1 == endpoint && _2);
const auto self = (endpoint.peer == _call->joinAs());
_viewport->add(
endpoint,
Calls::Group::VideoTileTrack{
Calls::GroupCall::TrackPointer(track),
row,
},
Calls::GroupCall::TrackSizeValue(track),
std::move(pinned),
self);
};
for (const auto &[endpoint, track] : _call->activeVideoTracks()) {
setupTile(endpoint, track);
}
_call->videoStreamActiveUpdates(
) | rpl::on_next([=](const Calls::VideoStateToggle &update) {
if (update.value) {
// Add async (=> the participant row is definitely in Members).
const auto endpoint = update.endpoint;
crl::on_main(_viewport->widget(), [=] {
const auto &tracks = _call->activeVideoTracks();
const auto i = tracks.find(endpoint);
if (i != end(tracks)) {
setupTile(endpoint, i->second);
}
});
} else {
// Remove sync.
_viewport->remove(update.endpoint);
}
}, _viewport->lifetime());
_viewport->qualityRequests(
) | rpl::on_next([=](const Calls::VideoQualityRequest &request) {
_call->requestVideoQuality(request.endpoint, request.quality);
}, _viewport->lifetime());
_loading->lower();
_viewport->widget()->lower();
_viewport->setControlsShown(0.);
_call->hasVideoWithFramesValue(
) | rpl::on_next([=](bool has) {
if (has) {
_loading = nullptr;
}
}, _loading->lifetime());
setVolume(Core::App().settings().videoVolume());
}
void VideoStream::setupMessages() {
_messages->hiddenShowRequested() | rpl::on_next([=] {
_call->messages()->requestHiddenShow();
}, _messages->lifetime());
_messages->deleteRequests(
) | rpl::on_next([=](Calls::Group::MessageDeleteRequest event) {
_call->messages()->deleteConfirmed(event);
}, _messages->lifetime());
}
void VideoStream::setVolume(float64 volume) {
const auto value = volume * Calls::Group::kDefaultVolume;
_call->changeVolume({
.peer = _call->peer(),
.volume = int(base::SafeRound(value)),
});
}
} // namespace Media::View

View File

@@ -0,0 +1,96 @@
/*
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
class Painter;
class QOpenGLFunctions;
namespace Calls {
class GroupCall;
} // namespace Calls
namespace Calls::Group {
class Members;
class Viewport;
class MessagesUi;
} // namespace Calls::Group
namespace ChatHelpers {
class Show;
} // namespace ChatHelpers
namespace Data {
class GroupCall;
struct MessageReactionsTopPaid;
} // namespace Data
namespace Ui {
class RpWidgetWrap;
} // namespace Ui
namespace Ui::GL {
enum class Backend;
} // namespace Ui::GL
namespace Media::View {
[[nodiscard]] auto TopVideoStreamDonors(not_null<Calls::GroupCall*> call)
-> rpl::producer<std::vector<Data::MessageReactionsTopPaid>>;
class VideoStream final {
public:
VideoStream(
not_null<QWidget*> parent,
not_null<Ui::RpWidgetWrap*> borrowedRp,
bool borrowedOpenGL,
Ui::GL::Backend backend,
std::shared_ptr<ChatHelpers::Show> show,
std::shared_ptr<Data::GroupCall> call,
QString callLinkSlug,
MsgId callJoinMessageId);
~VideoStream();
[[nodiscard]] not_null<Calls::GroupCall*> call() const;
[[nodiscard]] rpl::producer<> closeRequests() const;
void setVolume(float64 volume);
void updateGeometry(int x, int y, int width, int height);
void toggleCommentsOn(rpl::producer<bool> shown);
void ensureBorrowedRenderer(QOpenGLFunctions &f);
void borrowedPaint(QOpenGLFunctions &f);
void ensureBorrowedRenderer();
void borrowedPaint(Painter &p, const QRegion &clip);
[[nodiscard]] rpl::lifetime &lifetime();
private:
class Delegate;
class Loading;
void setupVideo();
void setupMembers();
void setupMessages();
rpl::variable<bool> _commentsShown;
std::shared_ptr<ChatHelpers::Show> _show;
std::unique_ptr<Delegate> _delegate;
std::unique_ptr<Loading> _loading;
std::unique_ptr<Calls::GroupCall> _call;
std::unique_ptr<Calls::Group::Members> _members;
std::unique_ptr<Calls::Group::Viewport> _viewport;
std::unique_ptr<Calls::Group::MessagesUi> _messages;
rpl::event_stream<> _closeRequests;
rpl::lifetime _lifetime;
};
} // namespace Media::View