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
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:
1182
Telegram/SourceFiles/media/view/media_view.style
Normal file
1182
Telegram/SourceFiles/media/view/media_view.style
Normal file
File diff suppressed because it is too large
Load Diff
915
Telegram/SourceFiles/media/view/media_view_group_thumbs.cpp
Normal file
915
Telegram/SourceFiles/media/view/media_view_group_thumbs.cpp
Normal 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
|
||||
152
Telegram/SourceFiles/media/view/media_view_group_thumbs.h
Normal file
152
Telegram/SourceFiles/media/view/media_view_group_thumbs.h
Normal 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
|
||||
28
Telegram/SourceFiles/media/view/media_view_open_common.cpp
Normal file
28
Telegram/SourceFiles/media/view/media_view_open_common.cpp
Normal 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
|
||||
173
Telegram/SourceFiles/media/view/media_view_open_common.h
Normal file
173
Telegram/SourceFiles/media/view/media_view_open_common.h
Normal 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
|
||||
1217
Telegram/SourceFiles/media/view/media_view_overlay_opengl.cpp
Normal file
1217
Telegram/SourceFiles/media/view/media_view_overlay_opengl.cpp
Normal file
File diff suppressed because it is too large
Load Diff
169
Telegram/SourceFiles/media/view/media_view_overlay_opengl.h
Normal file
169
Telegram/SourceFiles/media/view/media_view_overlay_opengl.h
Normal 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
|
||||
322
Telegram/SourceFiles/media/view/media_view_overlay_raster.cpp
Normal file
322
Telegram/SourceFiles/media/view/media_view_overlay_raster.cpp
Normal 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
|
||||
82
Telegram/SourceFiles/media/view/media_view_overlay_raster.h
Normal file
82
Telegram/SourceFiles/media/view/media_view_overlay_raster.h
Normal 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
|
||||
@@ -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
|
||||
7052
Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp
Normal file
7052
Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp
Normal file
File diff suppressed because it is too large
Load Diff
782
Telegram/SourceFiles/media/view/media_view_overlay_widget.h
Normal file
782
Telegram/SourceFiles/media/view/media_view_overlay_widget.h
Normal 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 ®ion);
|
||||
|
||||
[[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
|
||||
1878
Telegram/SourceFiles/media/view/media_view_pip.cpp
Normal file
1878
Telegram/SourceFiles/media/view/media_view_pip.cpp
Normal file
File diff suppressed because it is too large
Load Diff
319
Telegram/SourceFiles/media/view/media_view_pip.h
Normal file
319
Telegram/SourceFiles/media/view/media_view_pip.h
Normal 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
|
||||
812
Telegram/SourceFiles/media/view/media_view_pip_opengl.cpp
Normal file
812
Telegram/SourceFiles/media/view/media_view_pip_opengl.cpp
Normal 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
|
||||
128
Telegram/SourceFiles/media/view/media_view_pip_opengl.h
Normal file
128
Telegram/SourceFiles/media/view/media_view_pip_opengl.h
Normal 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
|
||||
228
Telegram/SourceFiles/media/view/media_view_pip_raster.cpp
Normal file
228
Telegram/SourceFiles/media/view/media_view_pip_raster.cpp
Normal 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
|
||||
66
Telegram/SourceFiles/media/view/media_view_pip_raster.h
Normal file
66
Telegram/SourceFiles/media/view/media_view_pip_raster.h
Normal 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
|
||||
37
Telegram/SourceFiles/media/view/media_view_pip_renderer.h
Normal file
37
Telegram/SourceFiles/media/view/media_view_pip_renderer.h
Normal 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
|
||||
452
Telegram/SourceFiles/media/view/media_view_playback_controls.cpp
Normal file
452
Telegram/SourceFiles/media/view/media_view_playback_controls.cpp
Normal 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
|
||||
133
Telegram/SourceFiles/media/view/media_view_playback_controls.h
Normal file
133
Telegram/SourceFiles/media/view/media_view_playback_controls.h
Normal 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
|
||||
168
Telegram/SourceFiles/media/view/media_view_playback_progress.cpp
Normal file
168
Telegram/SourceFiles/media/view/media_view_playback_progress.cpp
Normal 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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
379
Telegram/SourceFiles/media/view/media_view_video_stream.cpp
Normal file
379
Telegram/SourceFiles/media/view/media_view_video_stream.cpp
Normal 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
|
||||
96
Telegram/SourceFiles/media/view/media_view_video_stream.h
Normal file
96
Telegram/SourceFiles/media/view/media_view_video_stream.h
Normal 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
|
||||
Reference in New Issue
Block a user