AyuGramDesktop/Telegram/SourceFiles/media/view/media_view_pip.cpp
Ilya Fedin eb1ef6d2a7 Fix getting the screen of viewer/pip
We need to workaround getting the actual screen for the parent by getting its position yet we need to get the setted screen for the widget itself as that's the screen used to compute the geometry
2023-10-22 22:20:52 +04:00

1804 lines
50 KiB
C++

/*
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.h"
#include "media/streaming/media_streaming_player.h"
#include "media/streaming/media_streaming_document.h"
#include "media/streaming/media_streaming_utility.h"
#include "media/view/media_view_playback_progress.h"
#include "media/view/media_view_pip_opengl.h"
#include "media/view/media_view_pip_raster.h"
#include "media/audio/media_audio.h"
#include "data/data_document.h"
#include "data/data_document_media.h"
#include "data/data_file_origin.h"
#include "data/data_session.h"
#include "data/data_media_rotation.h"
#include "main/main_account.h"
#include "main/main_session.h"
#include "core/application.h"
#include "base/platform/base_platform_info.h"
#include "base/power_save_blocker.h"
#include "base/event_filter.h"
#include "ui/platform/ui_platform_utility.h"
#include "ui/platform/ui_platform_window_title.h"
#include "ui/widgets/buttons.h"
#include "ui/wrap/fade_wrap.h"
#include "ui/widgets/shadow.h"
#include "ui/text/format_values.h"
#include "ui/gl/gl_surface.h"
#include "ui/painter.h"
#include "window/window_controller.h"
#include "styles/style_widgets.h"
#include "styles/style_window.h"
#include "styles/style_media_view.h"
#include <QtGui/QWindow>
#include <QtGui/QScreen>
#include <QtWidgets/QApplication>
namespace Media {
namespace View {
namespace {
constexpr auto kPipLoaderPriority = 2;
constexpr auto kMsInSecond = 1000;
[[nodiscard]] QRect ScreenFromPosition(QPoint point) {
const auto screen = QGuiApplication::screenAt(point);
const auto use = screen ? screen : QGuiApplication::primaryScreen();
return use
? use->availableGeometry()
: QRect(0, 0, st::windowDefaultWidth, st::windowDefaultHeight);
}
[[nodiscard]] QSize MaxAllowedSizeForScreen(QSize screenSize) {
// Each side should be less than screen side - 3 * st::pipBorderSkip,
// That way it won't try to snap to both opposite sides of the screen.
const auto skip = 3 * st::pipBorderSkip;
return { screenSize.width() - skip, screenSize.height() - skip };
}
[[nodiscard]] QPoint ClampToEdges(QRect screen, QRect inner) {
const auto skip = st::pipBorderSkip;
const auto area = st::pipBorderSnapArea;
const auto sleft = screen.x() + skip;
const auto stop = screen.y() + skip;
const auto sright = screen.x() + screen.width() - skip;
const auto sbottom = screen.y() + screen.height() - skip;
const auto ileft = inner.x();
const auto itop = inner.y();
const auto iright = inner.x() + inner.width();
const auto ibottom = inner.y() + inner.height();
auto shiftx = 0;
auto shifty = 0;
if (iright + shiftx >= sright - area && iright + shiftx < sright + area) {
shiftx += (sright - iright);
}
if (ileft + shiftx >= sleft - area && ileft + shiftx < sleft + area) {
shiftx += (sleft - ileft);
}
if (ibottom + shifty >= sbottom - area && ibottom + shifty < sbottom + area) {
shifty += (sbottom - ibottom);
}
if (itop + shifty >= stop - area && itop + shifty < stop + area) {
shifty += (stop - itop);
}
return inner.topLeft() + QPoint(shiftx, shifty);
}
[[nodiscard]] QRect Transformed(
QRect original,
QSize minimalSize,
QSize maximalSize,
QPoint delta,
RectPart by) {
const auto width = original.width();
const auto height = original.height();
const auto x1 = width - minimalSize.width();
const auto x2 = maximalSize.width() - width;
const auto y1 = height - minimalSize.height();
const auto y2 = maximalSize.height() - height;
switch (by) {
case RectPart::Center: return original.translated(delta);
case RectPart::TopLeft:
original.setTop(original.y() + std::clamp(delta.y(), -y2, y1));
original.setLeft(original.x() + std::clamp(delta.x(), -x2, x1));
return original;
case RectPart::TopRight:
original.setTop(original.y() + std::clamp(delta.y(), -y2, y1));
original.setWidth(original.width() + std::clamp(delta.x(), -x1, x2));
return original;
case RectPart::BottomRight:
original.setHeight(
original.height() + std::clamp(delta.y(), -y1, y2));
original.setWidth(original.width() + std::clamp(delta.x(), -x1, x2));
return original;
case RectPart::BottomLeft:
original.setHeight(
original.height() + std::clamp(delta.y(), -y1, y2));
original.setLeft(original.x() + std::clamp(delta.x(), -x2, x1));
return original;
case RectPart::Left:
original.setLeft(original.x() + std::clamp(delta.x(), -x2, x1));
return original;
case RectPart::Top:
original.setTop(original.y() + std::clamp(delta.y(), -y2, y1));
return original;
case RectPart::Right:
original.setWidth(original.width() + std::clamp(delta.x(), -x1, x2));
return original;
case RectPart::Bottom:
original.setHeight(
original.height() + std::clamp(delta.y(), -y1, y2));
return original;
}
return original;
Unexpected("RectPart in PiP Transformed.");
}
[[nodiscard]] QRect Constrained(
QRect original,
QSize minimalSize,
QSize maximalSize,
QSize ratio,
RectPart by,
RectParts attached) {
if (by == RectPart::Center) {
return original;
} else if (!original.width() && !original.height()) {
return QRect(original.topLeft(), ratio);
}
const auto widthLarger = (original.width() * ratio.height())
> (original.height() * ratio.width());
const auto desiredSize = ratio.scaled(
original.size(),
(((RectParts(by) & RectPart::AllCorners)
|| ((by == RectPart::Top || by == RectPart::Bottom)
&& widthLarger)
|| ((by == RectPart::Left || by == RectPart::Right)
&& !widthLarger))
? Qt::KeepAspectRatio
: Qt::KeepAspectRatioByExpanding));
const auto newSize = QSize(
std::clamp(
desiredSize.width(),
minimalSize.width(),
maximalSize.width()),
std::clamp(
desiredSize.height(),
minimalSize.height(),
maximalSize.height()));
switch (by) {
case RectPart::TopLeft:
return QRect(
original.topLeft() + QPoint(
original.width() - newSize.width(),
original.height() - newSize.height()),
newSize);
case RectPart::TopRight:
return QRect(
original.topLeft() + QPoint(
0,
original.height() - newSize.height()),
newSize);
case RectPart::BottomRight:
return QRect(original.topLeft(), newSize);
case RectPart::BottomLeft:
return QRect(
original.topLeft() + QPoint(
original.width() - newSize.width(),
0),
newSize);
case RectPart::Left:
return QRect(
original.topLeft() + QPoint(
(original.width() - newSize.width()),
((attached & RectPart::Top)
? 0
: (attached & RectPart::Bottom)
? (original.height() - newSize.height())
: (original.height() - newSize.height()) / 2)),
newSize);
case RectPart::Top:
return QRect(
original.topLeft() + QPoint(
((attached & RectPart::Left)
? 0
: (attached & RectPart::Right)
? (original.width() - newSize.width())
: (original.width() - newSize.width()) / 2),
0),
newSize);
case RectPart::Right:
return QRect(
original.topLeft() + QPoint(
0,
((attached & RectPart::Top)
? 0
: (attached & RectPart::Bottom)
? (original.height() - newSize.height())
: (original.height() - newSize.height()) / 2)),
newSize);
case RectPart::Bottom:
return QRect(
original.topLeft() + QPoint(
((attached & RectPart::Left)
? 0
: (attached & RectPart::Right)
? (original.width() - newSize.width())
: (original.width() - newSize.width()) / 2),
(original.height() - newSize.height())),
newSize);
}
Unexpected("RectPart in PiP Constrained.");
}
[[nodiscard]] QByteArray Serialize(const PipPanel::Position &position) {
auto result = QByteArray();
auto stream = QDataStream(&result, QIODevice::WriteOnly);
stream.setVersion(QDataStream::Qt_5_3);
stream
<< qint32(position.attached.value())
<< qint32(position.snapped.value())
<< position.screen
<< position.geometry;
stream.device()->close();
return result;
}
[[nodiscard]] PipPanel::Position Deserialize(const QByteArray &data) {
auto stream = QDataStream(data);
auto result = PipPanel::Position();
auto attached = qint32(0);
auto snapped = qint32(0);
stream >> attached >> snapped >> result.screen >> result.geometry;
if (stream.status() != QDataStream::Ok) {
return {};
}
result.attached = RectParts::from_raw(attached);
result.snapped = RectParts::from_raw(snapped);
return result;
}
Qt::Edges RectPartToQtEdges(RectPart rectPart) {
switch (rectPart) {
case RectPart::TopLeft:
return Qt::TopEdge | Qt::LeftEdge;
case RectPart::TopRight:
return Qt::TopEdge | Qt::RightEdge;
case RectPart::BottomRight:
return Qt::BottomEdge | Qt::RightEdge;
case RectPart::BottomLeft:
return Qt::BottomEdge | Qt::LeftEdge;
case RectPart::Left:
return Qt::LeftEdge;
case RectPart::Top:
return Qt::TopEdge;
case RectPart::Right:
return Qt::RightEdge;
case RectPart::Bottom:
return Qt::BottomEdge;
}
return Qt::Edges();
}
} // namespace
QRect RotatedRect(QRect rect, int rotation) {
switch (rotation) {
case 0: return rect;
case 90: return QRect(
rect.y(),
-rect.x() - rect.width(),
rect.height(),
rect.width());
case 180: return QRect(
-rect.x() - rect.width(),
-rect.y() - rect.height(),
rect.width(),
rect.height());
case 270: return QRect(
-rect.y() - rect.height(),
rect.x(),
rect.height(),
rect.width());
}
Unexpected("Rotation in RotatedRect.");
}
bool UsePainterRotation(int rotation) {
return !(rotation % 180);
}
QSize FlipSizeByRotation(QSize size, int rotation) {
return (((rotation / 90) % 2) == 1)
? QSize(size.height(), size.width())
: size;
}
QImage RotateFrameImage(QImage image, int rotation) {
auto transform = QTransform();
transform.rotate(rotation);
return image.transformed(transform);
}
PipPanel::PipPanel(
QWidget *parent,
Fn<Ui::GL::ChosenRenderer(Ui::GL::Capabilities)> renderer)
: _content(Ui::GL::CreateSurface(std::move(renderer)))
, _parent(parent) {
}
void PipPanel::init() {
widget()->setWindowFlags(Qt::Tool
| Qt::WindowStaysOnTopHint
| Qt::FramelessWindowHint
| Qt::WindowDoesNotAcceptFocus);
widget()->setAttribute(Qt::WA_ShowWithoutActivating);
widget()->setAttribute(Qt::WA_MacAlwaysShowToolWindow);
widget()->setAttribute(Qt::WA_NoSystemBackground);
widget()->setAttribute(Qt::WA_TranslucentBackground);
Ui::Platform::IgnoreAllActivation(widget());
Ui::Platform::InitOnTopPanel(widget());
widget()->setMouseTracking(true);
widget()->resize(0, 0);
widget()->hide();
widget()->createWinId();
rp()->shownValue(
) | rpl::filter([=](bool shown) {
return shown;
}) | rpl::start_with_next([=] {
// Workaround Qt's forced transient parent.
Ui::Platform::ClearTransientParent(widget());
}, rp()->lifetime());
QObject::connect(
widget()->windowHandle(),
&QWindow::screenChanged,
[=](QScreen *screen) {
handleScreenChanged(screen);
});
if (Platform::IsWayland()) {
rp()->sizeValue(
) | rpl::start_with_next([=](QSize size) {
handleWaylandResize(size);
}, rp()->lifetime());
base::install_event_filter(widget(), [=](not_null<QEvent*> event) {
if (event->type() == QEvent::Resize && _inHandleWaylandResize) {
return base::EventFilterResult::Cancel;
}
return base::EventFilterResult::Continue;
});
base::install_event_filter(widget()->windowHandle(), [=](not_null<QEvent*> event) {
if (event->type() == QEvent::Resize) {
if (_inHandleWaylandResize) {
return base::EventFilterResult::Cancel;
}
const auto newSize = static_cast<QResizeEvent*>(event.get())->size();
if (_suggestedWaylandSize == newSize) {
handleWaylandResize(newSize);
return base::EventFilterResult::Cancel;
}
}
return base::EventFilterResult::Continue;
});
}
}
not_null<QWidget*> PipPanel::widget() const {
return _content->rpWidget();
}
not_null<Ui::RpWidgetWrap*> PipPanel::rp() const {
return _content.get();
}
void PipPanel::setAspectRatio(QSize ratio) {
if (_ratio == ratio) {
return;
}
_ratio = ratio;
if (_ratio.isEmpty()) {
_ratio = QSize(1, 1);
}
Ui::Platform::DisableSystemWindowResize(widget(), _ratio);
if (!widget()->size().isEmpty()) {
setPosition(countPosition());
}
}
void PipPanel::setPosition(Position position) {
if (!position.screen.isEmpty()) {
for (const auto screen : QApplication::screens()) {
if (screen->geometry() == position.screen) {
setPositionOnScreen(position, screen->availableGeometry());
return;
}
}
}
setPositionDefault();
}
QRect PipPanel::inner() const {
return widget()->rect().marginsRemoved(_padding);
}
RectParts PipPanel::attached() const {
return _attached;
}
bool PipPanel::useTransparency() const {
return _useTransparency;
}
void PipPanel::setDragDisabled(bool disabled) {
_dragDisabled = disabled;
if (_dragState) {
_dragState = std::nullopt;
}
}
bool PipPanel::dragging() const {
return _dragState.has_value();
}
rpl::producer<> PipPanel::saveGeometryRequests() const {
return _saveGeometryRequests.events();
}
QScreen *PipPanel::myScreen() const {
return widget()->screen();
}
PipPanel::Position PipPanel::countPosition() const {
const auto screen = myScreen();
if (!screen) {
return Position();
}
auto result = Position();
result.screen = screen->geometry();
result.geometry = widget()->geometry().marginsRemoved(_padding);
const auto available = screen->availableGeometry();
const auto skip = st::pipBorderSkip;
const auto left = result.geometry.x();
const auto right = left + result.geometry.width();
const auto top = result.geometry.y();
const auto bottom = top + result.geometry.height();
if ((!_dragState || *_dragState != RectPart::Center)
&& !Platform::IsWayland()) {
if (left == available.x()) {
result.attached |= RectPart::Left;
} else if (right == available.x() + available.width()) {
result.attached |= RectPart::Right;
} else if (left == available.x() + skip) {
result.snapped |= RectPart::Left;
} else if (right == available.x() + available.width() - skip) {
result.snapped |= RectPart::Right;
}
if (top == available.y()) {
result.attached |= RectPart::Top;
} else if (bottom == available.y() + available.height()) {
result.attached |= RectPart::Bottom;
} else if (top == available.y() + skip) {
result.snapped |= RectPart::Top;
} else if (bottom == available.y() + available.height() - skip) {
result.snapped |= RectPart::Bottom;
}
}
return result;
}
void PipPanel::setPositionDefault() {
const auto widgetScreen = [&](auto &&widget) -> QScreen* {
if (!widget) {
return nullptr;
}
if (!Platform::IsWayland()) {
if (const auto screen = QGuiApplication::screenAt(
widget->geometry().center())) {
return screen;
}
}
return widget->screen();
};
const auto parentScreen = widgetScreen(_parent);
const auto myScreen = widget()->screen();
if (parentScreen && myScreen != parentScreen) {
widget()->windowHandle()->setScreen(parentScreen);
}
auto position = Position();
position.snapped = RectPart::Top | RectPart::Left;
position.screen = parentScreen->geometry();
position.geometry = QRect(0, 0, st::pipDefaultSize, st::pipDefaultSize);
setPositionOnScreen(position, parentScreen->availableGeometry());
}
void PipPanel::setPositionOnScreen(Position position, QRect available) {
const auto screen = available;
const auto requestedSize = position.geometry.size();
const auto max = std::max(requestedSize.width(), requestedSize.height());
// Apply aspect ratio.
const auto scaled = (_ratio.width() > _ratio.height())
? QSize(max, max * _ratio.height() / _ratio.width())
: QSize(max * _ratio.width() / _ratio.height(), max);
// Apply maximum size.
const auto fit = MaxAllowedSizeForScreen(screen.size());
const auto byWidth = (scaled.width() * fit.height())
> (scaled.height() * fit.width());
const auto normalized = (byWidth && scaled.width() > fit.width())
? QSize(fit.width(), fit.width() * scaled.height() / scaled.width())
: (!byWidth && scaled.height() > fit.height())
? QSize(
fit.height() * scaled.width() / scaled.height(),
fit.height())
: scaled;
// Apply minimal size.
const auto min = st::pipMinimalSize;
const auto minimalSize = (_ratio.width() > _ratio.height())
? QSize(min * _ratio.width() / _ratio.height(), min)
: QSize(min, min * _ratio.height() / _ratio.width());
const auto size = QSize(
std::max(normalized.width(), minimalSize.width()),
std::max(normalized.height(), minimalSize.height()));
// Apply maximal size.
const auto maximalSize = byWidth
? QSize(fit.width(), fit.width() * _ratio.height() / _ratio.width())
: QSize(fit.height() * _ratio.width() / _ratio.height(), fit.height());
// Apply left-right screen borders.
const auto skip = st::pipBorderSkip;
const auto inner = screen.marginsRemoved({ skip, skip, skip, skip });
auto geometry = QRect(position.geometry.topLeft(), size);
if ((position.attached & RectPart::Left)
|| (geometry.x() < screen.x())) {
geometry.moveLeft(screen.x());
} else if ((position.attached & RectPart::Right)
|| (geometry.x() + geometry.width() > screen.x() + screen.width())) {
geometry.moveLeft(screen.x() + screen.width() - geometry.width());
} else if (position.snapped & RectPart::Left) {
geometry.moveLeft(inner.x());
} else if (position.snapped & RectPart::Right) {
geometry.moveLeft(inner.x() + inner.width() - geometry.width());
}
// Apply top-bottom screen borders.
if ((position.attached & RectPart::Top) || (geometry.y() < screen.y())) {
geometry.moveTop(screen.y());
} else if ((position.attached & RectPart::Bottom)
|| (geometry.y() + geometry.height()
> screen.y() + screen.height())) {
geometry.moveTop(
screen.y() + screen.height() - geometry.height());
} else if (position.snapped & RectPart::Top) {
geometry.moveTop(inner.y());
} else if (position.snapped & RectPart::Bottom) {
geometry.moveTop(inner.y() + inner.height() - geometry.height());
}
geometry += _padding;
setGeometry(geometry);
widget()->setMinimumSize(minimalSize);
widget()->setMaximumSize(
std::max(minimalSize.width(), maximalSize.width()),
std::max(minimalSize.height(), maximalSize.height()));
updateDecorations();
}
void PipPanel::update() {
widget()->update();
}
void PipPanel::setGeometry(QRect geometry) {
widget()->setGeometry(geometry);
}
void PipPanel::handleWaylandResize(QSize size) {
_inHandleWaylandResize = true;
_suggestedWaylandSize = size;
// Apply aspect ratio.
const auto max = std::max(size.width(), size.height());
const auto scaled = (_ratio.width() > _ratio.height())
? QSize(max, max * _ratio.height() / _ratio.width())
: QSize(max * _ratio.width() / _ratio.height(), max);
// Buffer can't be bigger than the configured
// (suggested by compositor) size.
const auto byWidth = (scaled.width() * size.height())
> (scaled.height() * size.width());
const auto normalized = (byWidth && scaled.width() > size.width())
? QSize(size.width(), size.width() * scaled.height() / scaled.width())
: (!byWidth && scaled.height() > size.height())
? QSize(
size.height() * scaled.width() / scaled.height(),
size.height())
: scaled;
widget()->resize(normalized);
_inHandleWaylandResize = false;
}
void PipPanel::handleScreenChanged(QScreen *screen) {
const auto screenGeometry = screen->availableGeometry();
const auto minimalSize = _ratio.scaled(
st::pipMinimalSize,
st::pipMinimalSize,
Qt::KeepAspectRatioByExpanding);
const auto maximalSize = _ratio.scaled(
MaxAllowedSizeForScreen(screenGeometry.size()),
Qt::KeepAspectRatio);
widget()->setMinimumSize(minimalSize);
widget()->setMaximumSize(
std::max(minimalSize.width(), maximalSize.width()),
std::max(minimalSize.height(), maximalSize.height()));
}
void PipPanel::handleMousePress(QPoint position, Qt::MouseButton button) {
if (button != Qt::LeftButton) {
return;
}
updateOverState(position);
_pressState = _overState;
_pressPoint = QCursor::pos();
}
void PipPanel::handleMouseRelease(QPoint position, Qt::MouseButton button) {
if (button != Qt::LeftButton || !base::take(_pressState)) {
return;
} else if (base::take(_dragState)) {
finishDrag(QCursor::pos());
updateOverState(position);
}
}
void PipPanel::updateOverState(QPoint point) {
const auto size = st::pipResizeArea;
const auto ignore = _attached | _snapped;
const auto count = [&](RectPart side, int padding) {
return (ignore & side) ? 0 : padding ? padding : size;
};
const auto left = count(RectPart::Left, _padding.left());
const auto top = count(RectPart::Top, _padding.top());
const auto right = count(RectPart::Right, _padding.right());
const auto bottom = count(RectPart::Bottom, _padding.bottom());
const auto width = widget()->width();
const auto height = widget()->height();
const auto overState = [&] {
if (point.x() < left) {
if (point.y() < top) {
return RectPart::TopLeft;
} else if (point.y() >= height - bottom) {
return RectPart::BottomLeft;
} else {
return RectPart::Left;
}
} else if (point.x() >= width - right) {
if (point.y() < top) {
return RectPart::TopRight;
} else if (point.y() >= height - bottom) {
return RectPart::BottomRight;
} else {
return RectPart::Right;
}
} else if (point.y() < top) {
return RectPart::Top;
} else if (point.y() >= height - bottom) {
return RectPart::Bottom;
} else {
return RectPart::Center;
}
}();
if (_overState != overState) {
_overState = overState;
widget()->setCursor([&] {
switch (_overState) {
case RectPart::Center:
return style::cur_pointer;
case RectPart::TopLeft:
case RectPart::BottomRight:
return style::cur_sizefdiag;
case RectPart::TopRight:
case RectPart::BottomLeft:
return style::cur_sizebdiag;
case RectPart::Left:
case RectPart::Right:
return style::cur_sizehor;
case RectPart::Top:
case RectPart::Bottom:
return style::cur_sizever;
}
Unexpected("State in PipPanel::updateOverState.");
}());
}
}
void PipPanel::handleMouseMove(QPoint position) {
if (!_pressState) {
updateOverState(position);
return;
}
const auto point = QCursor::pos();
const auto distance = QApplication::startDragDistance();
if (!_dragState
&& (point - _pressPoint).manhattanLength() > distance
&& !_dragDisabled) {
_dragState = _pressState;
updateDecorations();
_dragStartGeometry = widget()->geometry().marginsRemoved(_padding);
}
if (_dragState) {
if (Platform::IsWayland()) {
startSystemDrag();
} else {
processDrag(point);
}
}
}
void PipPanel::startSystemDrag() {
Expects(_dragState.has_value());
const auto stateEdges = RectPartToQtEdges(*_dragState);
if (stateEdges) {
widget()->windowHandle()->startSystemResize(stateEdges);
} else {
widget()->windowHandle()->startSystemMove();
}
Ui::SendSynteticMouseEvent(
widget().get(),
QEvent::MouseButtonRelease,
Qt::LeftButton);
}
void PipPanel::processDrag(QPoint point) {
Expects(_dragState.has_value());
const auto dragPart = *_dragState;
const auto screen = (dragPart == RectPart::Center)
? ScreenFromPosition(point)
: myScreen()
? myScreen()->availableGeometry()
: QRect();
if (screen.isEmpty()) {
return;
}
const auto minimalSize = _ratio.scaled(
st::pipMinimalSize,
st::pipMinimalSize,
Qt::KeepAspectRatioByExpanding);
const auto maximalSize = _ratio.scaled(
MaxAllowedSizeForScreen(screen.size()),
Qt::KeepAspectRatio);
const auto geometry = Transformed(
_dragStartGeometry,
minimalSize,
maximalSize,
point - _pressPoint,
dragPart);
const auto valid = Constrained(
geometry,
minimalSize,
maximalSize,
_ratio,
dragPart,
_attached);
const auto clamped = (dragPart == RectPart::Center)
? ClampToEdges(screen, valid)
: valid.topLeft();
widget()->setMinimumSize(minimalSize);
widget()->setMaximumSize(
std::max(minimalSize.width(), maximalSize.width()),
std::max(minimalSize.height(), maximalSize.height()));
if (clamped != valid.topLeft()) {
moveAnimated(clamped);
} else {
const auto newGeometry = valid.marginsAdded(_padding);
_positionAnimation.stop();
setGeometry(newGeometry);
}
}
void PipPanel::finishDrag(QPoint point) {
const auto screen = ScreenFromPosition(point);
const auto inner = widget()->geometry().marginsRemoved(_padding);
const auto position = widget()->pos();
const auto clamped = [&] {
auto result = position;
if (Platform::IsWayland()) {
return result;
}
if (result.x() > screen.x() + screen.width() - inner.width()) {
result.setX(screen.x() + screen.width() - inner.width());
}
if (result.x() < screen.x()) {
result.setX(screen.x());
}
if (result.y() > screen.y() + screen.height() - inner.height()) {
result.setY(screen.y() + screen.height() - inner.height());
}
if (result.y() < screen.y()) {
result.setY(screen.y());
}
return result;
}();
if (position != clamped) {
moveAnimated(clamped);
} else {
_positionAnimation.stop();
updateDecorations();
}
}
void PipPanel::updatePositionAnimated() {
const auto progress = _positionAnimation.value(1.);
if (!_positionAnimation.animating()) {
widget()->move(_positionAnimationTo
- QPoint(_padding.left(), _padding.top()));
if (!_dragState) {
updateDecorations();
}
return;
}
const auto from = QPointF(_positionAnimationFrom);
const auto to = QPointF(_positionAnimationTo);
widget()->move((from + (to - from) * progress).toPoint()
- QPoint(_padding.left(), _padding.top()));
}
void PipPanel::moveAnimated(QPoint to) {
if (_positionAnimation.animating() && _positionAnimationTo == to) {
return;
}
_positionAnimationTo = to;
_positionAnimationFrom = widget()->pos()
+ QPoint(_padding.left(), _padding.top());
_positionAnimation.stop();
_positionAnimation.start(
[=] { updatePositionAnimated(); },
0.,
1.,
st::slideWrapDuration,
anim::easeOutCirc);
}
void PipPanel::updateDecorations() {
const auto guard = gsl::finally([&] {
if (!_dragState) {
_saveGeometryRequests.fire({});
}
});
const auto position = countPosition();
const auto use = Ui::Platform::TranslucentWindowsSupported();
const auto full = use ? st::callShadow.extend : style::margins();
const auto padding = style::margins(
(position.attached & RectPart::Left) ? 0 : full.left(),
(position.attached & RectPart::Top) ? 0 : full.top(),
(position.attached & RectPart::Right) ? 0 : full.right(),
(position.attached & RectPart::Bottom) ? 0 : full.bottom());
_snapped = position.snapped;
if (_padding == padding && _attached == position.attached) {
return;
}
const auto newGeometry = position.geometry.marginsAdded(padding);
_attached = position.attached;
_padding = padding;
_useTransparency = use;
widget()->setAttribute(Qt::WA_OpaquePaintEvent, !_useTransparency);
setGeometry(newGeometry);
update();
}
Pip::Pip(
not_null<Delegate*> delegate,
not_null<DocumentData*> data,
std::shared_ptr<Streaming::Document> shared,
FnMut<void()> closeAndContinue,
FnMut<void()> destroy)
: _delegate(delegate)
, _data(data)
, _instance(std::move(shared), [=] { waitingAnimationCallback(); })
, _panel(
_delegate->pipParentWidget(),
[=](Ui::GL::Capabilities capabilities) {
return chooseRenderer(capabilities);
})
, _playbackProgress(std::make_unique<PlaybackProgress>())
, _rotation(data->owner().mediaRotation().get(data))
, _lastPositiveVolume((Core::App().settings().videoVolume() > 0.)
? Core::App().settings().videoVolume()
: Core::Settings::kDefaultVolume)
, _closeAndContinue(std::move(closeAndContinue))
, _destroy(std::move(destroy)) {
setupPanel();
setupButtons();
setupStreaming();
_data->session().account().sessionChanges(
) | rpl::start_with_next([=] {
_destroy();
}, _panel.rp()->lifetime());
}
Pip::~Pip() = default;
void Pip::setupPanel() {
_panel.init();
const auto size = [&] {
if (!_instance.info().video.size.isEmpty()) {
return _instance.info().video.size;
}
const auto media = _data->activeMediaView();
if (media) {
media->goodThumbnailWanted();
}
const auto good = media ? media->goodThumbnail() : nullptr;
const auto original = good ? good->size() : _data->dimensions;
return original.isEmpty() ? QSize(1, 1) : original;
}();
_panel.setAspectRatio(FlipSizeByRotation(size, _rotation));
_panel.setPosition(Deserialize(_delegate->pipLoadGeometry()));
_panel.widget()->show();
_panel.saveGeometryRequests(
) | rpl::start_with_next([=] {
saveGeometry();
}, _panel.rp()->lifetime());
_panel.rp()->events(
) | rpl::start_with_next([=](not_null<QEvent*> e) {
const auto mousePosition = [&] {
return static_cast<QMouseEvent*>(e.get())->pos();
};
const auto mouseButton = [&] {
return static_cast<QMouseEvent*>(e.get())->button();
};
switch (e->type()) {
case QEvent::Close: handleClose(); break;
case QEvent::Leave: handleLeave(); break;
case QEvent::MouseMove:
handleMouseMove(mousePosition());
break;
case QEvent::MouseButtonPress:
handleMousePress(mousePosition(), mouseButton());
break;
case QEvent::MouseButtonRelease:
handleMouseRelease(mousePosition(), mouseButton());
break;
case QEvent::MouseButtonDblClick:
handleDoubleClick(mouseButton());
break;
}
}, _panel.rp()->lifetime());
}
void Pip::handleClose() {
crl::on_main(_panel.widget(), [=] {
_destroy();
});
}
void Pip::handleLeave() {
setOverState(OverState::None);
}
void Pip::handleMouseMove(QPoint position) {
const auto weak = Ui::MakeWeak(_panel.widget());
const auto guard = gsl::finally([&] {
if (weak) {
_panel.handleMouseMove(position);
}
});
setOverState(computeState(position));
seekUpdate(position);
volumeControllerUpdate(position);
}
void Pip::setOverState(OverState state) {
if (_over == state) {
return;
}
const auto wasShown = ResolveShownOver(_over);
_over = state;
const auto nowAreShown = (ResolveShownOver(_over) != OverState::None);
if ((wasShown != OverState::None) != nowAreShown) {
_controlsShown.start(
[=] { _panel.update(); },
nowAreShown ? 0. : 1.,
nowAreShown ? 1. : 0.,
st::fadeWrapDuration,
anim::linear);
}
if (!_pressed) {
updateActiveState(wasShown);
}
_panel.update();
}
void Pip::setPressedState(std::optional<OverState> state) {
if (_pressed == state) {
return;
}
const auto wasShown = shownActiveState();
_pressed = state;
updateActiveState(wasShown);
}
Pip::OverState Pip::shownActiveState() const {
return ResolveShownOver(_pressed.value_or(_over));
}
float64 Pip::activeValue(const Button &button) const {
const auto shownState = ResolveShownOver(button.state);
return button.active.value((shownActiveState() == shownState) ? 1. : 0.);
}
void Pip::updateActiveState(OverState wasShown) {
const auto check = [&](Button &button) {
const auto shownState = ResolveShownOver(button.state);
const auto nowIsShown = (shownActiveState() == shownState);
if ((wasShown == shownState) != nowIsShown) {
button.active.start(
[=, &button] { _panel.widget()->update(button.icon); },
nowIsShown ? 0. : 1.,
nowIsShown ? 1. : 0.,
st::fadeWrapDuration,
anim::linear);
}
};
check(_close);
check(_enlarge);
check(_play);
check(_playback);
check(_volumeToggle);
check(_volumeController);
}
Pip::OverState Pip::ResolveShownOver(OverState state) {
return (state == OverState::VolumeController)
? OverState::VolumeToggle
: state;
}
void Pip::handleMousePress(QPoint position, Qt::MouseButton button) {
const auto weak = Ui::MakeWeak(_panel.widget());
const auto guard = gsl::finally([&] {
if (weak) {
_panel.handleMousePress(position, button);
}
});
if (button != Qt::LeftButton) {
return;
}
_pressed = _over;
if (_over == OverState::Playback || _over == OverState::VolumeController) {
_panel.setDragDisabled(true);
}
seekUpdate(position);
volumeControllerUpdate(position);
}
void Pip::handleMouseRelease(QPoint position, Qt::MouseButton button) {
const auto weak = Ui::MakeWeak(_panel.widget());
const auto guard = gsl::finally([&] {
if (weak) {
_panel.handleMouseRelease(position, button);
}
});
if (button != Qt::LeftButton) {
return;
}
seekUpdate(position);
volumeControllerUpdate(position);
const auto pressed = base::take(_pressed);
if (pressed && *pressed == OverState::Playback) {
_panel.setDragDisabled(false);
seekFinish(_playbackProgress->value());
} else if (pressed && *pressed == OverState::VolumeController) {
_panel.setDragDisabled(false);
_panel.update();
} else if (_panel.dragging() || !pressed || *pressed != _over) {
_lastHandledPress = std::nullopt;
} else {
_lastHandledPress = _over;
switch (_over) {
case OverState::Close: _panel.widget()->close(); break;
case OverState::Enlarge: _closeAndContinue(); break;
case OverState::VolumeToggle: volumeToggled(); break;
case OverState::Other: playbackPauseResume(); break;
}
}
}
void Pip::handleDoubleClick(Qt::MouseButton button) {
if (_over != OverState::Other
|| !_lastHandledPress
|| *_lastHandledPress != _over) {
return;
}
playbackPauseResume(); // Un-click the first click.
_closeAndContinue();
}
void Pip::seekUpdate(QPoint position) {
if (!_pressed || *_pressed != OverState::Playback) {
return;
}
const auto unbound = (position.x() - _playback.icon.x())
/ float64(_playback.icon.width());
const auto progress = std::clamp(unbound, 0., 1.);
seekProgress(progress);
}
void Pip::seekProgress(float64 value) {
if (!_lastDurationMs) {
return;
}
_playbackProgress->setValue(value, false);
const auto positionMs = std::clamp(
static_cast<crl::time>(value * _lastDurationMs),
crl::time(0),
_lastDurationMs);
if (_seekPositionMs != positionMs) {
_seekPositionMs = positionMs;
if (!_instance.player().paused()
&& !_instance.player().finished()) {
_pausedBySeek = true;
playbackPauseResume();
}
updatePlaybackTexts(_seekPositionMs, _lastDurationMs, kMsInSecond);
}
}
void Pip::seekFinish(float64 value) {
if (!_lastDurationMs) {
return;
}
const auto positionMs = std::clamp(
static_cast<crl::time>(value * _lastDurationMs),
crl::time(0),
_lastDurationMs);
_seekPositionMs = -1;
_startPaused = !_pausedBySeek && !_instance.player().finished();
restartAtSeekPosition(positionMs);
}
void Pip::volumeChanged(float64 volume) {
if (volume > 0.) {
_lastPositiveVolume = volume;
}
Player::mixer()->setVideoVolume(volume);
Core::App().settings().setVideoVolume(volume);
Core::App().saveSettingsDelayed();
}
void Pip::volumeToggled() {
const auto volume = Core::App().settings().videoVolume();
volumeChanged(volume ? 0. : _lastPositiveVolume);
_panel.update();
}
void Pip::volumeControllerUpdate(QPoint position) {
if (!_pressed || *_pressed != OverState::VolumeController) {
return;
}
const auto unbound = (position.x() - _volumeController.icon.x())
/ float64(_volumeController.icon.width());
const auto value = std::clamp(unbound, 0., 1.);
volumeChanged(value);
_panel.update();
}
void Pip::setupButtons() {
_close.state = OverState::Close;
_enlarge.state = OverState::Enlarge;
_playback.state = OverState::Playback;
_volumeToggle.state = OverState::VolumeToggle;
_volumeController.state = OverState::VolumeController;
_play.state = OverState::Other;
_panel.rp()->sizeValue(
) | rpl::map([=] {
return _panel.inner();
}) | rpl::start_with_next([=](QRect rect) {
const auto skip = st::pipControlSkip;
_close.area = QRect(
rect.x(),
rect.y(),
st::pipCloseIcon.width() + 2 * skip,
st::pipCloseIcon.height() + 2 * skip);
_enlarge.area = QRect(
_close.area.x() + _close.area.width(),
rect.y(),
st::pipEnlargeIcon.width() + 2 * skip,
st::pipEnlargeIcon.height() + 2 * skip);
const auto volumeSkip = st::pipPlaybackSkip;
const auto volumeHeight = 2 * volumeSkip + st::pipPlaybackWide;
const auto volumeToggleWidth = st::pipVolumeIcon0.width()
+ 2 * skip;
const auto volumeToggleHeight = st::pipVolumeIcon0.height()
+ 2 * skip;
const auto volumeWidth = (((st::mediaviewVolumeWidth + 2 * skip)
+ _close.area.width()
+ _enlarge.area.width()
+ volumeToggleWidth) < rect.width())
? st::mediaviewVolumeWidth
: 0;
_volumeController.area = QRect(
rect.x() + rect.width() - volumeWidth - 2 * volumeSkip,
rect.y() + (volumeToggleHeight - volumeHeight) / 2,
volumeWidth,
volumeHeight);
_volumeToggle.area = QRect(
_volumeController.area.x()
- st::pipVolumeIcon0.width()
- skip,
rect.y(),
volumeToggleWidth,
volumeToggleHeight);
if (!Ui::Platform::TitleControlsOnLeft()) {
_close.area.moveLeft(rect.x()
+ rect.width()
- (_close.area.x() - rect.x())
- _close.area.width());
_enlarge.area.moveLeft(rect.x()
+ rect.width()
- (_enlarge.area.x() - rect.x())
- _enlarge.area.width());
_volumeToggle.area.moveLeft(rect.x());
_volumeController.area.moveLeft(_volumeToggle.area.x()
+ _volumeToggle.area.width());
}
_close.icon = _close.area.marginsRemoved({ skip, skip, skip, skip });
_enlarge.icon = _enlarge.area.marginsRemoved(
{ skip, skip, skip, skip });
_volumeToggle.icon = _volumeToggle.area.marginsRemoved(
{ skip, skip, skip, skip });
_play.icon = QRect(
rect.x() + (rect.width() - st::pipPlayIcon.width()) / 2,
rect.y() + (rect.height() - st::pipPlayIcon.height()) / 2,
st::pipPlayIcon.width(),
st::pipPlayIcon.height());
const auto volumeArea = _volumeController.area;
_volumeController.icon = (volumeArea.width() > 2 * volumeSkip
&& volumeArea.height() > 2 * volumeSkip)
? volumeArea.marginsRemoved(
{ volumeSkip, volumeSkip, volumeSkip, volumeSkip })
: QRect();
const auto playbackSkip = st::pipPlaybackSkip;
const auto playbackHeight = 2 * playbackSkip + st::pipPlaybackWide;
_playback.area = QRect(
rect.x(),
rect.y() + rect.height() - playbackHeight,
rect.width(),
playbackHeight);
_playback.icon = _playback.area.marginsRemoved(
{ playbackSkip, playbackSkip, playbackSkip, playbackSkip });
}, _panel.rp()->lifetime());
_playbackProgress->setValueChangedCallback([=](
float64 value,
float64 receivedTill) {
_panel.widget()->update(_playback.area);
});
}
void Pip::saveGeometry() {
_delegate->pipSaveGeometry(Serialize(_panel.countPosition()));
}
void Pip::updatePlayPauseResumeState(const Player::TrackState &state) {
auto showPause = Player::ShowPauseIcon(state.state);
if (showPause != _showPause) {
_showPause = showPause;
_panel.update();
}
}
void Pip::setupStreaming() {
_instance.setPriority(kPipLoaderPriority);
_instance.lockPlayer();
_instance.player().updates(
) | rpl::start_with_next_error([=](Streaming::Update &&update) {
handleStreamingUpdate(std::move(update));
}, [=](Streaming::Error &&error) {
handleStreamingError(std::move(error));
}, _instance.lifetime());
updatePlaybackState();
}
Ui::GL::ChosenRenderer Pip::chooseRenderer(
Ui::GL::Capabilities capabilities) {
const auto use = Platform::IsMac()
? true
: capabilities.transparency;
LOG(("OpenGL: %1 (PipPanel)").arg(Logs::b(use)));
if (use) {
_opengl = true;
return {
.renderer = std::make_unique<RendererGL>(this),
.backend = Ui::GL::Backend::OpenGL,
};
}
return {
.renderer = std::make_unique<RendererSW>(this),
.backend = Ui::GL::Backend::Raster,
};
}
void Pip::paint(not_null<Renderer*> renderer) const {
const auto controlsShown = _controlsShown.value(
(_over != OverState::None) ? 1. : 0.);
auto geometry = ContentGeometry{
.inner = _panel.inner(),
.attached = (_panel.useTransparency()
? _panel.attached()
: RectPart::AllSides),
.fade = controlsShown,
.outer = _panel.widget()->size(),
.rotation = _rotation,
.videoRotation = _instance.info().video.rotation,
.useTransparency = _panel.useTransparency(),
};
if (canUseVideoFrame()) {
renderer->paintTransformedVideoFrame(geometry);
_instance.markFrameShown();
} else {
const auto content = staticContent();
if (_preparedCoverState == ThumbState::Cover) {
geometry.rotation += base::take(geometry.videoRotation);
}
renderer->paintTransformedStaticContent(content, geometry);
}
if (_instance.waitingShown()) {
renderer->paintRadialLoading(countRadialRect(), controlsShown);
}
if (controlsShown > 0) {
paintButtons(renderer, controlsShown);
paintPlayback(renderer, controlsShown);
paintVolumeController(renderer, controlsShown);
}
}
void Pip::paintButtons(not_null<Renderer*> renderer, float64 shown) const {
const auto outer = _panel.widget()->width();
const auto drawOne = [&](
const Button &button,
const style::icon &icon,
const style::icon &iconOver) {
renderer->paintButton(
button,
outer,
shown,
activeValue(button),
icon,
iconOver);
};
renderer->paintButtonsStart();
drawOne(
_play,
_showPause ? st::pipPauseIcon : st::pipPlayIcon,
_showPause ? st::pipPauseIconOver : st::pipPlayIconOver);
drawOne(_close, st::pipCloseIcon, st::pipCloseIconOver);
drawOne(_enlarge, st::pipEnlargeIcon, st::pipEnlargeIconOver);
const auto volume = Core::App().settings().videoVolume();
if (volume <= 0.) {
drawOne(
_volumeToggle,
st::pipVolumeIcon0,
st::pipVolumeIcon0Over);
} else if (volume < 1 / 2.) {
drawOne(
_volumeToggle,
st::pipVolumeIcon1,
st::pipVolumeIcon1Over);
} else {
drawOne(
_volumeToggle,
st::pipVolumeIcon2,
st::pipVolumeIcon2Over);
}
}
void Pip::paintPlayback(not_null<Renderer*> renderer, float64 shown) const {
const auto outer = QRect(
_playback.icon.x(),
_playback.icon.y() - st::pipPlaybackFont->height,
_playback.icon.width(),
st::pipPlaybackFont->height + _playback.icon.height());
renderer->paintPlayback(outer, shown);
}
void Pip::paintPlaybackContent(
QPainter &p,
QRect outer,
float64 shown) const {
p.setOpacity(shown);
paintPlaybackProgress(p, outer);
paintPlaybackTexts(p, outer);
}
void Pip::paintPlaybackProgress(QPainter &p, QRect outer) const {
const auto radius = _playback.icon.height() / 2;
const auto progress = _playbackProgress->value();
const auto active = activeValue(_playback);
const auto height = anim::interpolate(
st::pipPlaybackWidth,
_playback.icon.height(),
active);
const auto rect = QRect(
outer.x(),
(outer.y()
+ st::pipPlaybackFont->height
+ _playback.icon.height()
- height),
outer.width(),
height);
paintProgressBar(p, rect, progress, radius, active);
}
void Pip::paintProgressBar(
QPainter &p,
const QRect &rect,
float64 progress,
int radius,
float64 active) const {
const auto done = int(base::SafeRound(rect.width() * progress));
PainterHighQualityEnabler hq(p);
p.setPen(Qt::NoPen);
if (done > 0) {
p.setBrush(anim::brush(
st::mediaviewPipControlsFg,
st::mediaviewPipPlaybackActive,
active));
p.setClipRect(rect.x(), rect.y(), done, rect.height());
p.drawRoundedRect(
rect.x(),
rect.y(),
std::min(done + radius, rect.width()),
rect.height(),
radius,
radius);
}
if (done < rect.width()) {
const auto from = std::max(rect.x() + done - radius, rect.x());
p.setBrush(st::mediaviewPipPlaybackInactive);
p.setClipRect(
rect.x() + done,
rect.y(),
rect.width() - done,
rect.height());
p.drawRoundedRect(
from,
rect.y(),
rect.x() + rect.width() - from,
rect.height(),
radius,
radius);
}
p.setClipping(false);
}
void Pip::paintPlaybackTexts(QPainter &p, QRect outer) const {
const auto left = outer.x()
- _playback.icon.x()
+ _playback.area.x()
+ st::pipPlaybackTextSkip;
const auto right = outer.x()
- _playback.icon.x()
+ _playback.area.x()
+ _playback.area.width()
- st::pipPlaybackTextSkip;
const auto top = outer.y() + st::pipPlaybackFont->ascent;
p.setFont(st::pipPlaybackFont);
p.setPen(st::mediaviewPipControlsFgOver);
p.drawText(left, top, _timeAlready);
p.drawText(right - _timeLeftWidth, top, _timeLeft);
}
void Pip::paintVolumeController(
not_null<Renderer*> renderer,
float64 shown) const {
if (_volumeController.icon.isEmpty()) {
return;
}
renderer->paintVolumeController(_volumeController.icon, shown);
}
void Pip::paintVolumeControllerContent(
QPainter &p,
QRect outer,
float64 shown) const {
p.setOpacity(shown);
const auto radius = _volumeController.icon.height() / 2;
const auto volume = Core::App().settings().videoVolume();
const auto active = activeValue(_volumeController);
const auto height = anim::interpolate(
st::pipPlaybackWidth,
_volumeController.icon.height(),
active);
const auto rect = QRect(
outer.x(),
outer.y() + radius - height / 2,
outer.width(),
height);
paintProgressBar(p, rect, volume, radius, active);
}
void Pip::handleStreamingUpdate(Streaming::Update &&update) {
using namespace Streaming;
v::match(update.data, [&](Information &update) {
_panel.setAspectRatio(
FlipSizeByRotation(update.video.size, _rotation));
}, [&](const PreloadedVideo &update) {
updatePlaybackState();
}, [&](const UpdateVideo &update) {
_panel.update();
Core::App().updateNonIdle();
updatePlaybackState();
}, [&](const PreloadedAudio &update) {
updatePlaybackState();
}, [&](const UpdateAudio &update) {
updatePlaybackState();
}, [&](WaitingForData) {
}, [&](MutedByOther) {
}, [&](Finished) {
updatePlaybackState();
});
}
void Pip::updatePlaybackState() {
const auto state = _instance.player().prepareLegacyState();
updatePlayPauseResumeState(state);
if (state.position == kTimeUnknown
|| state.length == kTimeUnknown
|| _pausedBySeek) {
return;
}
_playbackProgress->updateState(state);
updatePowerSaveBlocker(state);
qint64 position = 0;
if (Player::IsStoppedAtEnd(state.state)) {
position = state.length;
} else if (!Player::IsStoppedOrStopping(state.state)) {
position = state.position;
} else {
position = 0;
}
const auto playFrequency = state.frequency;
_lastDurationMs = (state.length * crl::time(1000)) / playFrequency;
if (_seekPositionMs < 0) {
updatePlaybackTexts(position, state.length, playFrequency);
}
}
void Pip::updatePowerSaveBlocker(const Player::TrackState &state) {
const auto block = _data->isVideoFile()
&& !IsPausedOrPausing(state.state)
&& !IsStoppedOrStopping(state.state);
base::UpdatePowerSaveBlocker(
_powerSaveBlocker,
block,
base::PowerSaveBlockType::PreventDisplaySleep,
[] { return u"Video playback is active"_q; },
[=] { return _panel.widget()->windowHandle(); });
}
void Pip::updatePlaybackTexts(
int64 position,
int64 length,
int64 frequency) {
const auto playAlready = position / frequency;
const auto playLeft = (length / frequency) - playAlready;
const auto already = Ui::FormatDurationText(playAlready);
const auto minus = QChar(8722);
const auto left = minus + Ui::FormatDurationText(playLeft);
if (_timeAlready == already && _timeLeft == left) {
return;
}
_timeAlready = already;
_timeLeft = left;
_timeLeftWidth = st::pipPlaybackFont->width(_timeLeft);
_panel.widget()->update(QRect(
_playback.area.x(),
_playback.icon.y() - st::pipPlaybackFont->height,
_playback.area.width(),
st::pipPlaybackFont->height));
}
void Pip::handleStreamingError(Streaming::Error &&error) {
_panel.widget()->close();
}
void Pip::playbackPauseResume() {
if (_instance.player().failed()) {
_panel.widget()->close();
} else if (_instance.player().finished()
|| !_instance.player().active()) {
_startPaused = false;
restartAtSeekPosition(0);
} else if (_instance.player().paused()) {
_instance.resume();
updatePlaybackState();
} else {
_instance.pause();
updatePlaybackState();
}
}
void Pip::restartAtSeekPosition(crl::time position) {
if (!_instance.info().video.cover.isNull()) {
_preparedCoverStorage = QImage();
_preparedCoverState = ThumbState::Empty;
_instance.saveFrameToCover();
}
auto options = Streaming::PlaybackOptions();
options.position = position;
options.hwAllowed = Core::App().settings().hardwareAcceleratedVideo();
options.audioId = _instance.player().prepareLegacyState().id;
options.speed = _delegate->pipPlaybackSpeed();
_instance.play(options);
if (_startPaused) {
_instance.pause();
}
_pausedBySeek = false;
updatePlaybackState();
}
bool Pip::canUseVideoFrame() const {
return _instance.player().ready()
&& !_instance.info().video.cover.isNull();
}
QImage Pip::videoFrame(const FrameRequest &request) const {
Expects(canUseVideoFrame());
return _instance.frame(request);
}
Streaming::FrameWithInfo Pip::videoFrameWithInfo() const {
Expects(canUseVideoFrame());
return _instance.frameWithInfo();
}
QImage Pip::staticContent() const {
const auto &cover = _instance.info().video.cover;
const auto media = _data->activeMediaView();
const auto use = media
? media
: _data->inlineThumbnailBytes().isEmpty()
? nullptr
: _data->createMediaView();
if (use) {
use->goodThumbnailWanted();
}
const auto good = use ? use->goodThumbnail() : nullptr;
const auto thumb = use ? use->thumbnail() : nullptr;
const auto blurred = use ? use->thumbnailInline() : nullptr;
const auto state = !cover.isNull()
? ThumbState::Cover
: good
? ThumbState::Good
: thumb
? ThumbState::Thumb
: blurred
? ThumbState::Inline
: ThumbState::Empty;
if (!_preparedCoverStorage.isNull() && _preparedCoverState >= state) {
return _preparedCoverStorage;
}
_preparedCoverState = state;
if (state == ThumbState::Cover) {
_preparedCoverStorage = _instance.info().video.cover;
} else {
_preparedCoverStorage = (good
? good
: thumb
? thumb
: blurred
? blurred
: Image::BlankMedia().get())->original();
if (!good) {
_preparedCoverStorage = Images::Blur(
std::move(_preparedCoverStorage));
}
}
return _preparedCoverStorage;
}
void Pip::paintRadialLoadingContent(
QPainter &p,
const QRect &inner,
QColor fg) const {
const auto arc = inner.marginsRemoved(QMargins(
st::radialLine,
st::radialLine,
st::radialLine,
st::radialLine));
p.setOpacity(_instance.waitingOpacity());
p.setPen(Qt::NoPen);
p.setBrush(st::radialBg);
{
PainterHighQualityEnabler hq(p);
p.drawEllipse(inner);
}
p.setOpacity(1.);
Ui::InfiniteRadialAnimation::Draw(
p,
_instance.waitingState(),
arc.topLeft(),
arc.size(),
_panel.widget()->width(),
fg,
st::radialLine);
}
QRect Pip::countRadialRect() const {
const auto outer = _panel.inner();
return {
outer.x() + (outer.width() - st::radialSize.width()) / 2,
outer.y() + (outer.height() - st::radialSize.height()) / 2,
st::radialSize.width(),
st::radialSize.height()
};
}
Pip::OverState Pip::computeState(QPoint position) const {
if (!_panel.inner().contains(position)) {
return OverState::None;
} else if (_close.area.contains(position)) {
return OverState::Close;
} else if (_enlarge.area.contains(position)) {
return OverState::Enlarge;
} else if (_playback.area.contains(position)) {
return OverState::Playback;
} else if (_volumeToggle.area.contains(position)) {
return OverState::VolumeToggle;
} else if (_volumeController.area.contains(position)) {
return OverState::VolumeController;
} else {
return OverState::Other;
}
}
void Pip::waitingAnimationCallback() {
_panel.widget()->update(countRadialRect());
}
} // namespace View
} // namespace Media