From ec468431b489e35f7eee004da54d0a00680c01e7 Mon Sep 17 00:00:00 2001 From: John Preston Date: Sun, 23 May 2021 17:08:52 +0400 Subject: [PATCH] Raster render of wide mode in single widget. --- Telegram/CMakeLists.txt | 8 + .../calls/group/calls_group_call.h | 10 + .../calls/group/calls_group_large_video.cpp | 54 +-- .../calls/group/calls_group_large_video.h | 6 - .../calls/group/calls_group_panel.cpp | 217 ++------- .../calls/group/calls_group_panel.h | 6 +- .../calls/group/calls_group_viewport.cpp | 459 ++++++++++++++++++ .../calls/group/calls_group_viewport.h | 124 +++++ .../group/calls_group_viewport_opengl.cpp | 247 ++++++++++ .../calls/group/calls_group_viewport_opengl.h | 52 ++ .../group/calls_group_viewport_raster.cpp | 221 +++++++++ .../calls/group/calls_group_viewport_raster.h | 53 ++ .../calls/group/calls_group_viewport_tile.cpp | 126 +++++ .../calls/group/calls_group_viewport_tile.h | 82 ++++ 14 files changed, 1424 insertions(+), 241 deletions(-) create mode 100644 Telegram/SourceFiles/calls/group/calls_group_viewport.cpp create mode 100644 Telegram/SourceFiles/calls/group/calls_group_viewport.h create mode 100644 Telegram/SourceFiles/calls/group/calls_group_viewport_opengl.cpp create mode 100644 Telegram/SourceFiles/calls/group/calls_group_viewport_opengl.h create mode 100644 Telegram/SourceFiles/calls/group/calls_group_viewport_raster.cpp create mode 100644 Telegram/SourceFiles/calls/group/calls_group_viewport_raster.h create mode 100644 Telegram/SourceFiles/calls/group/calls_group_viewport_tile.cpp create mode 100644 Telegram/SourceFiles/calls/group/calls_group_viewport_tile.h diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt index f6e7bd8015..da57f0d433 100644 --- a/Telegram/CMakeLists.txt +++ b/Telegram/CMakeLists.txt @@ -289,6 +289,14 @@ PRIVATE calls/group/calls_group_panel.h calls/group/calls_group_settings.cpp calls/group/calls_group_settings.h + calls/group/calls_group_viewport.cpp + calls/group/calls_group_viewport.h + calls/group/calls_group_viewport_opengl.cpp + calls/group/calls_group_viewport_opengl.h + calls/group/calls_group_viewport_raster.cpp + calls/group/calls_group_viewport_raster.h + calls/group/calls_group_viewport_tile.cpp + calls/group/calls_group_viewport_tile.h calls/group/calls_volume_item.cpp calls/group/calls_volume_item.h calls/calls_box_controller.cpp diff --git a/Telegram/SourceFiles/calls/group/calls_group_call.h b/Telegram/SourceFiles/calls/group/calls_group_call.h index b5f6683a16..59d09a28b4 100644 --- a/Telegram/SourceFiles/calls/group/calls_group_call.h +++ b/Telegram/SourceFiles/calls/group/calls_group_call.h @@ -124,6 +124,16 @@ inline bool operator>=( return !(a < b); } +struct VideoPinToggle { + VideoEndpoint endpoint; + bool pinned = false; +}; + +struct VideoQualityRequest { + VideoEndpoint endpoint; + Group::VideoQuality quality = Group::VideoQuality(); +}; + struct VideoParams { std::string endpoint; QByteArray json; diff --git a/Telegram/SourceFiles/calls/group/calls_group_large_video.cpp b/Telegram/SourceFiles/calls/group/calls_group_large_video.cpp index a7686bc2e9..426f9c354e 100644 --- a/Telegram/SourceFiles/calls/group/calls_group_large_video.cpp +++ b/Telegram/SourceFiles/calls/group/calls_group_large_video.cpp @@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "calls/group/calls_group_large_video.h" +#include "calls/group/calls_group_viewport.h" // GenerateShadow. #include "calls/group/calls_group_common.h" #include "calls/group/calls_group_members_row.h" #include "media/view/media_view_pip.h" @@ -20,8 +21,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "lang/lang_keys.h" #include "styles/style_calls.h" -#include "core/application.h" - #include #include #include @@ -590,9 +589,9 @@ void LargeVideo::setGeometry(int x, int y, int width, int height) { if (width > 0 && height > 0) { const auto kMedium = style::ConvertScale(380); const auto kSmall = style::ConvertScale(200); - _requestedQuality = (width > kMedium || height > kMedium) + _requestedQuality = (width > kMedium && height > kMedium) ? VideoQuality::Full - : (width > kSmall || height > kSmall) + : (width > kSmall && height > kSmall) ? VideoQuality::Medium : VideoQuality::Thumbnail; } @@ -901,51 +900,4 @@ void LargeVideo::paintControls(Painter &p, QRect clip) { _track.row->name().drawLeftElided(p, nameLeft, nameTop, hasWidth, width); } -QImage GenerateShadow( - int height, - int topAlpha, - int bottomAlpha, - QColor color) { - Expects(topAlpha >= 0 && topAlpha < 256); - Expects(bottomAlpha >= 0 && bottomAlpha < 256); - Expects(height * style::DevicePixelRatio() < 65536); - - const auto base = (uint32(color.red()) << 16) - | (uint32(color.green()) << 8) - | uint32(color.blue()); - const auto premultiplied = (topAlpha == bottomAlpha) || !base; - auto result = QImage( - QSize(1, height * style::DevicePixelRatio()), - (premultiplied - ? QImage::Format_ARGB32_Premultiplied - : QImage::Format_ARGB32)); - if (topAlpha == bottomAlpha) { - color.setAlpha(topAlpha); - result.fill(color); - return result; - } - constexpr auto kShift = 16; - constexpr auto kMultiply = (1U << kShift); - const auto values = std::abs(topAlpha - bottomAlpha); - const auto rows = uint32(result.height()); - const auto step = (values * kMultiply) / (rows - 1); - const auto till = rows * uint32(step); - Assert(result.bytesPerLine() == sizeof(uint32)); - auto ints = reinterpret_cast(result.bits()); - if (topAlpha < bottomAlpha) { - for (auto i = uint32(0); i != till; i += step) { - *ints++ = base | ((topAlpha + (i >> kShift)) << 24); - } - } else { - for (auto i = uint32(0); i != till; i += step) { - *ints++ = base | ((topAlpha - (i >> kShift)) << 24); - } - } - if (!premultiplied) { - result = std::move(result).convertToFormat( - QImage::Format_ARGB32_Premultiplied); - } - return result; -} - } // namespace Calls::Group diff --git a/Telegram/SourceFiles/calls/group/calls_group_large_video.h b/Telegram/SourceFiles/calls/group/calls_group_large_video.h index e3a14e24b9..4d854ebe9e 100644 --- a/Telegram/SourceFiles/calls/group/calls_group_large_video.h +++ b/Telegram/SourceFiles/calls/group/calls_group_large_video.h @@ -112,10 +112,4 @@ private: }; -[[nodiscard]] QImage GenerateShadow( - int height, - int topAlpha, - int bottomAlpha, - QColor color = QColor(0, 0, 0)); - } // namespace Calls::Group diff --git a/Telegram/SourceFiles/calls/group/calls_group_panel.cpp b/Telegram/SourceFiles/calls/group/calls_group_panel.cpp index 2f25fae781..84886a161d 100644 --- a/Telegram/SourceFiles/calls/group/calls_group_panel.cpp +++ b/Telegram/SourceFiles/calls/group/calls_group_panel.cpp @@ -12,6 +12,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "calls/group/calls_group_settings.h" #include "calls/group/calls_group_menu.h" #include "calls/group/calls_group_large_video.h" +#include "calls/group/calls_group_viewport.h" #include "calls/group/ui/desktop_capture_choose_source.h" #include "ui/platform/ui_platform_window_title.h" #include "ui/platform/ui_platform_utility.h" @@ -379,11 +380,6 @@ std::unique_ptr InviteContactsController::createRow( } // namespace -struct Panel::VideoTile { - std::unique_ptr video; - VideoEndpoint endpoint; -}; - struct Panel::ControlsBackgroundNarrow { explicit ControlsBackgroundNarrow(not_null parent) : shadow(parent) @@ -442,10 +438,8 @@ Panel::Panel(not_null call) } Panel::~Panel() { - if (_menu) { - _menu.destroy(); - } - _videoTiles.clear(); + _menu.destroy(); + _viewport = nullptr; } void Panel::setupRealCallViewers() { @@ -999,7 +993,7 @@ void Panel::setupMembers() { _call->videoEndpointPinnedValue( ) | rpl::start_with_next([=](const VideoEndpoint &pinned) { if (mode() == PanelMode::Wide) { - refreshTilesGeometry(); + _viewport->showLarge(pinned); } else if (pinned) { enlargeVideo(); } @@ -1093,195 +1087,62 @@ void Panel::raiseControls() { _mute->raise(); } -void Panel::refreshTilesGeometry() { - const auto outer = _pinnedVideoWrap->size(); - if (_videoTiles.empty() - || outer.isEmpty() - || mode() == PanelMode::Default) { - return; - } - struct Geometry { - QSize size; - QRect columns; - QRect rows; - }; - const auto &pinned = _call->videoEndpointPinned(); - auto sizes = base::flat_map, Geometry>(); - sizes.reserve(_videoTiles.size()); - for (const auto &tile : _videoTiles) { - const auto video = tile.video.get(); - const auto size = (pinned && tile.endpoint != pinned) - ? QSize() - : video->trackSize(); - if (size.isEmpty()) { - video->setGeometry(0, 0, outer.width(), 0); - } else { - sizes.emplace(video, Geometry{ size }); - } - } - if (sizes.size() == 1) { - sizes.front().first->setGeometry(0, 0, outer.width(), outer.height()); - return; - } - if (sizes.empty()) { - return; - } - - auto columnsBlack = uint64(); - auto rowsBlack = uint64(); - const auto count = int(sizes.size()); - const auto skip = st::groupCallVideoLargeSkip; - const auto slices = int(std::ceil(std::sqrt(float64(count)))); - { - auto index = 0; - const auto columns = slices; - const auto sizew = (outer.width() + skip) / float64(columns); - for (auto column = 0; column != columns; ++column) { - const auto left = int(std::round(column * sizew)); - const auto width = int(std::round(column * sizew + sizew - skip)) - - left; - const auto rows = int(std::round((count - index) - / float64(columns - column))); - const auto sizeh = (outer.height() + skip) / float64(rows); - for (auto row = 0; row != rows; ++row) { - const auto top = int(std::round(row * sizeh)); - const auto height = int(std::round( - row * sizeh + sizeh - skip)) - top; - auto &geometry = (sizes.begin() + index)->second; - geometry.columns = { - left, - top, - width, - height }; - const auto scaled = geometry.size.scaled( - width, - height, - Qt::KeepAspectRatio); - columnsBlack += (scaled.width() < width) - ? (width - scaled.width()) * height - : (height - scaled.height()) * width; - ++index; - } - } - } - { - auto index = 0; - const auto rows = slices; - const auto sizeh = (outer.height() + skip) / float64(rows); - for (auto row = 0; row != rows; ++row) { - const auto top = int(std::round(row * sizeh)); - const auto height = int(std::round(row * sizeh + sizeh - skip)) - - top; - const auto columns = int(std::round((count - index) - / float64(rows - row))); - const auto sizew = (outer.width() + skip) / float64(columns); - for (auto column = 0; column != columns; ++column) { - const auto left = int(std::round(column * sizew)); - const auto width = int(std::round( - column * sizew + sizew - skip)) - left; - auto &geometry = (sizes.begin() + index)->second; - geometry.rows = { - left, - top, - width, - height }; - const auto scaled = geometry.size.scaled( - width, - height, - Qt::KeepAspectRatio); - rowsBlack += (scaled.width() < width) - ? (width - scaled.width()) * height - : (height - scaled.height()) * width; - ++index; - } - } - } - for (const auto &[video, geometry] : sizes) { - const auto &rect = (columnsBlack < rowsBlack) - ? geometry.columns - : geometry.rows; - video->setGeometry(rect.x(), rect.y(), rect.width(), rect.height()); - } -} - void Panel::setupPinnedVideo() { using namespace rpl::mappers; - _pinnedVideoWrap = std::make_unique(widget()); - const auto raw = _pinnedVideoWrap.get(); + _viewport = std::make_unique(widget(), _mode.current()); + const auto raw = _viewport.get(); const auto setupTile = [=]( const VideoEndpoint &endpoint, const GroupCall::VideoTrack &track) { const auto row = _members->lookupRow(track.peer); Assert(row != nullptr); - auto video = std::make_unique( - raw, - st::groupCallLargeVideoWide, - (mode() == PanelMode::Wide), - rpl::single(LargeVideoTrack{ track.track.get(), row }), + _viewport->add( + endpoint, + LargeVideoTrack{ track.track.get(), row }, _call->videoEndpointPinnedValue() | rpl::map(_1 == endpoint)); - - video->pinToggled( - ) | rpl::start_with_next([=](bool pinned) { - _call->pinVideoEndpoint(pinned ? endpoint : VideoEndpoint{}); - }, video->lifetime()); - - video->requestedQuality( - ) | rpl::start_with_next([=](VideoQuality quality) { - _call->requestVideoQuality(endpoint, quality); - }, video->lifetime()); - - video->trackSizeValue( - ) | rpl::start_with_next([=] { - refreshTilesGeometry(); - }, video->lifetime()); - - return VideoTile{ - .video = std::move(video), - .endpoint = endpoint, - }; }; for (const auto &[endpoint, track] : _call->activeVideoTracks()) { - _videoTiles.push_back(setupTile(endpoint, track)); + setupTile(endpoint, track); } _call->videoStreamActiveUpdates( ) | rpl::start_with_next([=](const VideoEndpoint &endpoint) { if (_call->activeVideoTracks().contains(endpoint)) { // Add async (=> the participant row is definitely in Members). - crl::on_main(raw, [=] { + crl::on_main(widget(), [=] { const auto &tracks = _call->activeVideoTracks(); const auto i = tracks.find(endpoint); if (i != end(tracks)) { - _videoTiles.push_back(setupTile(endpoint, i->second)); + setupTile(endpoint, i->second); } }); } else { // Remove sync. - const auto eraseTill = end(_videoTiles); - const auto eraseFrom = ranges::remove( - _videoTiles, - endpoint, - &VideoTile::endpoint); - if (eraseFrom != eraseTill) { - _videoTiles.erase(eraseFrom, eraseTill); - refreshTilesGeometry(); - } + _viewport->remove(endpoint); } }, raw->lifetime()); - raw->sizeValue() | rpl::start_with_next([=] { - refreshTilesGeometry(); + raw->pinToggled( + ) | rpl::start_with_next([=](const VideoPinToggle &value) { + _call->pinVideoEndpoint( + value.pinned ? value.endpoint : VideoEndpoint{}); }, raw->lifetime()); - raw->events( - ) | rpl::start_with_next([=](not_null e) { - if (e->type() == QEvent::Enter) { - Ui::Integration::Instance().registerLeaveSubscription(raw); - toggleWideControls(true); - } else if (e->type() == QEvent::Leave) { - Ui::Integration::Instance().unregisterLeaveSubscription(raw); - toggleWideControls(false); - } + raw->clicks( + ) | rpl::filter([=] { + return (_mode.current() == PanelMode::Default); + }) | rpl::start_with_next([=] { + enlargeVideo(); + }, raw->lifetime()); + + raw->qualityRequests( + ) | rpl::start_with_next([=](const VideoQualityRequest &request) { + _call->requestVideoQuality(request.endpoint, request.quality); + }, raw->lifetime()); + + raw->mouseInsideValue( + ) | rpl::start_with_next([=](bool inside) { + toggleWideControls(inside); }, raw->lifetime()); raiseControls(); @@ -1811,14 +1672,10 @@ bool Panel::updateMode() { if (_members) { _members->setMode(mode); } - if (_pinnedVideoWrap) { + if (_viewport) { _wideControlsShown = _showWideControls = true; _wideControlsAnimation.stop(); - _pinnedVideoWrap->setVisible(mode == PanelMode::Wide); - for (const auto &tile : _videoTiles) { - tile.video->setVisible(mode == PanelMode::Wide); - tile.video->setControlsShown(1.); - } + _viewport->setMode(mode); } updateButtonsStyles(); refreshControlsBackground(); @@ -2085,8 +1942,8 @@ void Panel::updateButtonsGeometry() { _wideControlsShown ? 1. : 0.); toggle(shown > 0.); - for (const auto &tile : _videoTiles) { - tile.video->setControlsShown(shown); + if (_viewport) { + _viewport->setControlsShown(shown); } const auto buttonsTop = widget()->height() - anim::interpolate( @@ -2206,7 +2063,7 @@ void Panel::updateMembersGeometry() { top, membersWidth, std::min(desiredHeight, widget()->height() - top - skip)); - _pinnedVideoWrap->setGeometry( + _viewport->widget()->setGeometry( skip, top, widget()->width() - membersWidth - 3 * skip, diff --git a/Telegram/SourceFiles/calls/group/calls_group_panel.h b/Telegram/SourceFiles/calls/group/calls_group_panel.h index 714b549d5d..26b6292cd1 100644 --- a/Telegram/SourceFiles/calls/group/calls_group_panel.h +++ b/Telegram/SourceFiles/calls/group/calls_group_panel.h @@ -54,7 +54,7 @@ struct CallBodyLayout; namespace Calls::Group { class Members; -class LargeVideo; +class Viewport; enum class PanelMode; class Panel final : private Ui::DesktopCapture::ChooseSourceDelegate { @@ -70,7 +70,6 @@ public: private: using State = GroupCall::State; - struct VideoTile; struct ControlsBackgroundNarrow; [[nodiscard]] not_null widget() const; @@ -155,8 +154,7 @@ private: object_ptr _menu = { nullptr }; object_ptr _joinAsToggle = { nullptr }; object_ptr _members = { nullptr }; - std::unique_ptr _pinnedVideoWrap; - std::vector _videoTiles; + std::unique_ptr _viewport; rpl::lifetime _trackControlsLifetime; rpl::lifetime _trackControlsOverStateLifetime; object_ptr _startsIn = { nullptr }; diff --git a/Telegram/SourceFiles/calls/group/calls_group_viewport.cpp b/Telegram/SourceFiles/calls/group/calls_group_viewport.cpp new file mode 100644 index 0000000000..b4abce3474 --- /dev/null +++ b/Telegram/SourceFiles/calls/group/calls_group_viewport.cpp @@ -0,0 +1,459 @@ +/* +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 "calls/group/calls_group_viewport.h" + +#include "calls/group/calls_group_large_video.h" // LargeVideoTrack. +#include "calls/group/calls_group_viewport_tile.h" +#include "calls/group/calls_group_viewport_opengl.h" +#include "calls/group/calls_group_viewport_raster.h" +#include "calls/group/calls_group_common.h" +#include "calls/group/calls_group_call.h" +#include "calls/group/calls_group_members_row.h" +#include "media/view/media_view_pip.h" +#include "base/platform/base_platform_info.h" +#include "webrtc/webrtc_video_track.h" +#include "ui/painter.h" +#include "ui/abstract_button.h" +#include "ui/gl/gl_surface.h" +#include "ui/effects/animations.h" +#include "ui/effects/cross_line.h" +#include "lang/lang_keys.h" +#include "styles/style_calls.h" + +#include +#include + +namespace Calls::Group { + +Viewport::Viewport(QWidget *parent, PanelMode mode) +: _mode(mode) +, _content(Ui::GL::CreateSurface( + parent, + [=](Ui::GL::Capabilities capabilities) { + return chooseRenderer(capabilities); + })) { + setup(); +} + +Viewport::~Viewport() { + base::take(_tiles); +} + +not_null Viewport::widget() const { + return _content->rpWidget(); +} + +not_null Viewport::rp() const { + return _content.get(); +} + +void Viewport::setup() { + const auto raw = widget(); + + raw->resize(0, 0); + raw->setAttribute(Qt::WA_OpaquePaintEvent); + raw->setMouseTracking(true); + + _content->sizeValue( + ) | rpl::start_with_next([=] { + updateTilesGeometry(); + }, lifetime()); + + _content->events( + ) | rpl::start_with_next([=](not_null e) { + const auto type = e->type(); + if (type == QEvent::Enter) { + Ui::Integration::Instance().registerLeaveSubscription(raw); + _mouseInside = true; + } else if (type == QEvent::Leave) { + Ui::Integration::Instance().unregisterLeaveSubscription(raw); + setSelected({}); + _mouseInside = false; + } else if (type == QEvent::MouseButtonPress) { + handleMousePress( + static_cast(e.get())->pos(), + static_cast(e.get())->button()); + } else if (type == QEvent::MouseButtonRelease) { + handleMouseRelease( + static_cast(e.get())->pos(), + static_cast(e.get())->button()); + } else if (type == QEvent::MouseMove) { + handleMouseMove(static_cast(e.get())->pos()); + } + }, lifetime()); +} + +bool Viewport::wide() const { + return (_mode.current() == PanelMode::Wide); +} + +void Viewport::setMode(PanelMode mode) { + if (_mode.current() == mode) { + return; + } + _mode = mode; + widget()->setVisible(wide()); // #TODO calls + setControlsShown(1.); + updateTilesGeometry(); + if (_mouseInside.current()) { + handleMouseMove(widget()->mapFromGlobal(QCursor::pos())); + } + if (!wide()) { + for (const auto &tile : _tiles) { + tile->togglePinShown(false); + } + } else if (_selected.tile) { + _selected.tile->togglePinShown(true); + } +} + +void Viewport::handleMousePress(QPoint position, Qt::MouseButton button) { + handleMouseMove(position); + + if (button == Qt::LeftButton) { + setPressed(_selected); + } +} + +void Viewport::handleMouseRelease(QPoint position, Qt::MouseButton button) { + handleMouseMove(position); + const auto pressed = _pressed; + setPressed({}); + if (const auto tile = pressed.tile) { + if (pressed == _selected) { + if (!wide()) { + _clicks.fire_copy(tile->endpoint()); + } else if (pressed.element == Selection::Element::PinButton) { + _pinToggles.fire({ + .endpoint = tile->endpoint(), + .pinned = !tile->pinned(), + }); + } + } + } +} + +void Viewport::handleMouseMove(QPoint position) { + if (!widget()->rect().contains(position)) { + setSelected({}); + return; + } + for (const auto &tile : _tiles) { + const auto geometry = tile->geometry(); + if (geometry.contains(position)) { + const auto pin = wide() + && tile->pinOuter().contains(position - geometry.topLeft()); + setSelected({ + .tile = tile.get(), + .element = (pin + ? Selection::Element::PinButton + : Selection::Element::Tile), + }); + return; + } + } + setSelected({}); +} + +void Viewport::setControlsShown(float64 shown) { + _controlsShownRatio = shown; + widget()->update(); +} + +void Viewport::add( + const VideoEndpoint &endpoint, + LargeVideoTrack track, + rpl::producer pinned) { + _tiles.push_back(std::make_unique( + endpoint, + track, + std::move(pinned), + [=] { widget()->update(); })); + + //video->pinToggled( // #TODO calls + //) | rpl::start_with_next([=](bool pinned) { + // _call->pinVideoEndpoint(pinned ? endpoint : VideoEndpoint{}); + //}, video->lifetime()); + + _tiles.back()->trackSizeValue( + ) | rpl::start_with_next([=] { + updateTilesGeometry(); + }, _tiles.back()->lifetime()); +} + +void Viewport::remove(const VideoEndpoint &endpoint) { + const auto i = ranges::find(_tiles, endpoint, &VideoTile::endpoint); + if (i == end(_tiles)) { + return; + } + const auto removing = i->get(); + if (_large == removing) { + _large = nullptr; + } + if (_selected.tile == removing) { + setSelected({}); + } + if (_pressed.tile == removing) { + setPressed({}); + } + _tiles.erase(i); + updateTilesGeometry(); +} + +void Viewport::showLarge(const VideoEndpoint &endpoint) { + const auto i = ranges::find(_tiles, endpoint, &VideoTile::endpoint); + const auto large = (i != end(_tiles)) ? i->get() : nullptr; + if (_large != large) { + _large = large; + updateTilesGeometry(); + } +} + +void Viewport::updateTilesGeometry() { + const auto outer = widget()->size(); + if (_tiles.empty() || outer.isEmpty()) { + return; + } + + const auto guard = gsl::finally([&] { widget()->update(); }); + + struct Geometry { + QSize size; + QRect columns; + QRect rows; + }; + auto sizes = base::flat_map, Geometry>(); + sizes.reserve(_tiles.size()); + for (const auto &tile : _tiles) { + const auto video = tile.get(); + const auto size = (_large && video != _large) + ? QSize() + : video->trackSize(); + if (size.isEmpty()) { + setTileGeometry(video, { 0, 0, outer.width(), 0 }); + } else { + sizes.emplace(video, Geometry{ size }); + } + } + if (sizes.size() == 1) { + setTileGeometry( + sizes.front().first, + { 0, 0, outer.width(), outer.height() }); + return; + } + if (sizes.empty()) { + return; + } + + auto columnsBlack = uint64(); + auto rowsBlack = uint64(); + const auto count = int(sizes.size()); + const auto skip = st::groupCallVideoLargeSkip; + const auto slices = int(std::ceil(std::sqrt(float64(count)))); + { + auto index = 0; + const auto columns = slices; + const auto sizew = (outer.width() + skip) / float64(columns); + for (auto column = 0; column != columns; ++column) { + const auto left = int(std::round(column * sizew)); + const auto width = int(std::round(column * sizew + sizew - skip)) + - left; + const auto rows = int(std::round((count - index) + / float64(columns - column))); + const auto sizeh = (outer.height() + skip) / float64(rows); + for (auto row = 0; row != rows; ++row) { + const auto top = int(std::round(row * sizeh)); + const auto height = int(std::round( + row * sizeh + sizeh - skip)) - top; + auto &geometry = (sizes.begin() + index)->second; + geometry.columns = { + left, + top, + width, + height }; + const auto scaled = geometry.size.scaled( + width, + height, + Qt::KeepAspectRatio); + columnsBlack += (scaled.width() < width) + ? (width - scaled.width()) * height + : (height - scaled.height()) * width; + ++index; + } + } + } + { + auto index = 0; + const auto rows = slices; + const auto sizeh = (outer.height() + skip) / float64(rows); + for (auto row = 0; row != rows; ++row) { + const auto top = int(std::round(row * sizeh)); + const auto height = int(std::round(row * sizeh + sizeh - skip)) + - top; + const auto columns = int(std::round((count - index) + / float64(rows - row))); + const auto sizew = (outer.width() + skip) / float64(columns); + for (auto column = 0; column != columns; ++column) { + const auto left = int(std::round(column * sizew)); + const auto width = int(std::round( + column * sizew + sizew - skip)) - left; + auto &geometry = (sizes.begin() + index)->second; + geometry.rows = { + left, + top, + width, + height }; + const auto scaled = geometry.size.scaled( + width, + height, + Qt::KeepAspectRatio); + rowsBlack += (scaled.width() < width) + ? (width - scaled.width()) * height + : (height - scaled.height()) * width; + ++index; + } + } + } + const auto layout = (columnsBlack < rowsBlack) + ? &Geometry::columns + : &Geometry::rows; + for (const auto &[video, geometry] : sizes) { + setTileGeometry(video, geometry.*layout); + } +} + +void Viewport::setTileGeometry(not_null tile, QRect geometry) { + tile->setGeometry(geometry); + + const auto min = std::min(geometry.width(), geometry.height()); + const auto kMedium = style::ConvertScale(480); + const auto kSmall = style::ConvertScale(240); + const auto quality = (min >= kMedium) + ? VideoQuality::Full + : (min >= kSmall) + ? VideoQuality::Medium + : VideoQuality::Thumbnail; + if (tile->updateRequestedQuality(quality)) { + _qualityRequests.fire(VideoQualityRequest{ + .endpoint = tile->endpoint(), + .quality = quality, + }); + } +} + +void Viewport::setSelected(Selection value) { + if (_selected == value) { + return; + } + if (_selected.tile) { + _selected.tile->togglePinShown(false); + } + _selected = value; + if (_selected.tile && wide()) { + _selected.tile->togglePinShown(true); + } + const auto pointer = _selected.tile + && (!wide() || _selected.element == Selection::Element::PinButton); + widget()->setCursor(pointer ? style::cur_pointer : style::cur_default); +} + +void Viewport::setPressed(Selection value) { + if (_pressed == value) { + return; + } + _pressed = value; +} + +Ui::GL::ChosenRenderer Viewport::chooseRenderer( + Ui::GL::Capabilities capabilities) { + const auto use = Platform::IsMac() + ? true + : Platform::IsWindows() + ? capabilities.supported + : capabilities.transparency; + LOG(("OpenGL: %1 (Calls::Group::Viewport)").arg(Logs::b(use))); + if (use) { + return { + .renderer = std::make_unique(this), + .backend = Ui::GL::Backend::OpenGL, + }; + } + return { + .renderer = std::make_unique(this), + .backend = Ui::GL::Backend::Raster, + }; +} + +[[nodiscard]] rpl::producer Viewport::pinToggled() const { + return _pinToggles.events(); +} + +[[nodiscard]] rpl::producer Viewport::clicks() const { + return _clicks.events(); +} + +[[nodiscard]] rpl::producer Viewport::qualityRequests() const { + return _qualityRequests.events(); +} + +[[nodiscard]] rpl::producer Viewport::mouseInsideValue() const { + return _mouseInside.value(); +} + +[[nodiscard]] rpl::lifetime &Viewport::lifetime() { + return _content->lifetime(); +} + +QImage GenerateShadow( + int height, + int topAlpha, + int bottomAlpha, + QColor color) { + Expects(topAlpha >= 0 && topAlpha < 256); + Expects(bottomAlpha >= 0 && bottomAlpha < 256); + Expects(height * style::DevicePixelRatio() < 65536); + + const auto base = (uint32(color.red()) << 16) + | (uint32(color.green()) << 8) + | uint32(color.blue()); + const auto premultiplied = (topAlpha == bottomAlpha) || !base; + auto result = QImage( + QSize(1, height * style::DevicePixelRatio()), + (premultiplied + ? QImage::Format_ARGB32_Premultiplied + : QImage::Format_ARGB32)); + if (topAlpha == bottomAlpha) { + color.setAlpha(topAlpha); + result.fill(color); + return result; + } + constexpr auto kShift = 16; + constexpr auto kMultiply = (1U << kShift); + const auto values = std::abs(topAlpha - bottomAlpha); + const auto rows = uint32(result.height()); + const auto step = (values * kMultiply) / (rows - 1); + const auto till = rows * uint32(step); + Assert(result.bytesPerLine() == sizeof(uint32)); + auto ints = reinterpret_cast(result.bits()); + if (topAlpha < bottomAlpha) { + for (auto i = uint32(0); i != till; i += step) { + *ints++ = base | ((topAlpha + (i >> kShift)) << 24); + } + } else { + for (auto i = uint32(0); i != till; i += step) { + *ints++ = base | ((topAlpha - (i >> kShift)) << 24); + } + } + if (!premultiplied) { + result = std::move(result).convertToFormat( + QImage::Format_ARGB32_Premultiplied); + } + return result; +} + +} // namespace Calls::Group diff --git a/Telegram/SourceFiles/calls/group/calls_group_viewport.h b/Telegram/SourceFiles/calls/group/calls_group_viewport.h new file mode 100644 index 0000000000..4df1e9f008 --- /dev/null +++ b/Telegram/SourceFiles/calls/group/calls_group_viewport.h @@ -0,0 +1,124 @@ +/* +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" + +namespace style { +struct GroupCallLargeVideo; +} // namespace style + +namespace Ui { +class AbstractButton; +class RpWidgetWrap; +namespace GL { +struct Capabilities; +struct ChosenRenderer; +} // namespace GL +} // namespace Ui + +namespace Calls { +struct VideoEndpoint; +struct VideoPinToggle; +struct VideoQualityRequest; +} // namespace Calls + +namespace Webrtc { +class VideoTrack; +} // namespace Webrtc + +namespace Calls::Group { + +class MembersRow; +enum class PanelMode; +enum class VideoQuality; +struct LargeVideoTrack; + +class Viewport final { +public: + Viewport(QWidget *parent, PanelMode mode); + ~Viewport(); + + [[nodiscard]] not_null widget() const; + [[nodiscard]] not_null rp() const; + + void setMode(PanelMode mode); + void setControlsShown(float64 shown); + + void add( + const VideoEndpoint &endpoint, + LargeVideoTrack track, + rpl::producer pinned); + void remove(const VideoEndpoint &endpoint); + void showLarge(const VideoEndpoint &endpoint); + + [[nodiscard]] rpl::producer pinToggled() const; + [[nodiscard]] rpl::producer clicks() const; + [[nodiscard]] rpl::producer qualityRequests() const; + [[nodiscard]] rpl::producer mouseInsideValue() const; + + [[nodiscard]] rpl::lifetime &lifetime(); + +private: + struct PinButton; + class VideoTile; + class Renderer; + class RendererGL; + + struct Selection { + enum class Element { + None, + Tile, + PinButton, + }; + VideoTile *tile = nullptr; + Element element = Element::None; + + inline bool operator==(Selection other) const { + return (tile == other.tile) && (element == other.element); + } + }; + + void setup(); + [[nodiscard]] bool wide() const; + + void updateTilesGeometry(); + void setTileGeometry(not_null tile, QRect geometry); + + void setSelected(Selection value); + void setPressed(Selection value); + + void handleMousePress(QPoint position, Qt::MouseButton button); + void handleMouseRelease(QPoint position, Qt::MouseButton button); + void handleMouseMove(QPoint position); + + [[nodiscard]] Ui::GL::ChosenRenderer chooseRenderer( + Ui::GL::Capabilities capabilities); + + rpl::variable _mode; + const std::unique_ptr _content; + std::vector> _tiles; + QImage _shadow; + rpl::event_stream _clicks; + rpl::event_stream _pinToggles; + rpl::event_stream _qualityRequests; + float64 _controlsShownRatio = 1.; + VideoTile *_large = nullptr; + Selection _selected; + Selection _pressed; + rpl::variable _mouseInside = false; + +}; + +[[nodiscard]] QImage GenerateShadow( + int height, + int topAlpha, + int bottomAlpha, + QColor color = QColor(0, 0, 0)); + +} // namespace Calls::Group diff --git a/Telegram/SourceFiles/calls/group/calls_group_viewport_opengl.cpp b/Telegram/SourceFiles/calls/group/calls_group_viewport_opengl.cpp new file mode 100644 index 0000000000..54139e8fc6 --- /dev/null +++ b/Telegram/SourceFiles/calls/group/calls_group_viewport_opengl.cpp @@ -0,0 +1,247 @@ +/* +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 "calls/group/calls_group_viewport_opengl.h" + +#include + +namespace Calls::Group { +namespace { + +struct ShaderPart { + QString header; + QString body; +}; + +[[nodiscard]] QString VertexShader(const std::vector &parts) { + const auto accumulate = [&](auto proj) { + return ranges::accumulate(parts, QString(), std::plus<>(), proj); + }; + return R"( +#version 130 +in vec2 position; +)" + accumulate(&ShaderPart::header) + R"( +void main() { + vec4 result = vec4(position, 0., 1.); +)" + accumulate(&ShaderPart::body) + R"( + gl_Position = result; +} +)"; +} + +[[nodiscard]] QString FragmentShader(const std::vector &parts) { + const auto accumulate = [&](auto proj) { + return ranges::accumulate(parts, QString(), std::plus<>(), proj); + }; + return R"( +#version 130 +out vec4 fragColor; +)" + accumulate(&ShaderPart::header) + R"( +void main() { + vec4 result = vec4(0., 0., 0., 0.); +)" + accumulate(&ShaderPart::body) + R"( + fragColor = result; +} +)"; +} + +[[nodiscard]] ShaderPart VertexPassTextureCoord() { + return { + .header = R"( +in vec2 texcoord; +out vec2 v_texcoord; +)", + .body = R"( + v_texcoord = texcoord; +)", + }; +} + +[[nodiscard]] ShaderPart FragmentSampleTexture() { + return { + .header = R"( +in vec2 v_texcoord; +uniform sampler2D s_texture; +)", + .body = R"( + result = texture(s_texture, v_texcoord); + result = vec4(result.b, result.g, result.r, result.a); +)", + }; +} + +[[nodiscard]] ShaderPart VertexViewportTransform() { + return { + .header = R"( +uniform vec2 viewport; +vec4 transform(vec4 position) { + return vec4( + vec2(-1, -1) + 2 * position.xy / viewport, + position.z, + position.w); +} +)", + .body = R"( + result = transform(result); +)", + }; +} + +[[nodiscard]] ShaderPart FragmentRoundCorners() { + return { + .header = R"( +uniform vec2 viewport; +uniform float roundRadius; +float roundedCorner() { + vec2 viewportHalf = viewport / 2; + vec2 fromViewportCenter = abs(gl_FragCoord.xy - viewportHalf); + vec2 vectorRadius = vec2(roundRadius + 0.5, roundRadius + 0.5); + vec2 fromCenterWithRadius = fromViewportCenter + vectorRadius; + vec2 fromRoundingCenter = max(fromCenterWithRadius, viewportHalf) + - viewportHalf; + float d = length(fromRoundingCenter) - roundRadius; + return 1. - smoothstep(0., 1., d); +} +)", + .body = R"( + result = vec4(result.r, result.g, result.b, result.a * roundedCorner()); +)", + }; +} + +[[nodiscard]] ShaderPart FragmentStaticColor() { + return { + .header = R"( +uniform vec4 s_color; +)", + .body = R"( + result = s_color; +)", + }; +} + +not_null MakeShader( + not_null program, + QOpenGLShader::ShaderType type, + const QString &source) { + const auto result = new QOpenGLShader(type, program); + if (!result->compileSourceCode(source)) { + LOG(("Shader Compilation Failed: %1, error %2." + ).arg(source + ).arg(result->log())); + } + program->addShader(result); + return result; +} + +void LinkProgram( + not_null program, + const QString &vertexSource, + const QString &fragmentSource) { + MakeShader(program, QOpenGLShader::Vertex, vertexSource); + MakeShader(program, QOpenGLShader::Fragment, fragmentSource); + if (!program->link()) { + LOG(("Shader Link Failed: %1.").arg(program->log())); + } +} + +class Quads final { +public: + void fill(QRect rect); + void paint( + not_null f, + not_null buffer, + not_null program, + QSize viewport, + const QColor &color, + Fn additional = nullptr); + +private: + static constexpr auto kMaxTriangles = 8; + std::array coordinates{ 0 }; + int triangles = 0; + +}; + +void Quads::fill(QRect rect) { + Expects(triangles + 2 <= kMaxTriangles); + + auto i = triangles * 6; + coordinates[i + 0] = coordinates[i + 10] = rect.x(); + coordinates[i + 1] = coordinates[i + 11] = rect.y(); + coordinates[i + 2] = rect.x() + rect.width(); + coordinates[i + 3] = rect.y(); + coordinates[i + 4] = coordinates[i + 6] = rect.x() + rect.width(); + coordinates[i + 5] = coordinates[i + 7] = rect.y() + rect.height(); + coordinates[i + 8] = rect.x(); + coordinates[i + 9] = rect.y() + rect.height(); + triangles += 2; +} + +void Quads::paint( + not_null f, + not_null buffer, + not_null program, + QSize viewport, + const QColor &color, + Fn additional) { + if (!triangles) { + return; + } + buffer->bind(); + buffer->allocate(coordinates.data(), triangles * 6 * sizeof(GLfloat)); + + f->glUseProgram(program->programId()); + program->setUniformValue("viewport", QSizeF(viewport)); + program->setUniformValue("s_color", QVector4D( + color.redF(), + color.greenF(), + color.blueF(), + color.alphaF())); + + GLint position = program->attributeLocation("position"); + f->glVertexAttribPointer(position, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(GLfloat), (void*)0); + f->glEnableVertexAttribArray(position); + + if (additional) { + additional(); + } + + f->glDrawArrays(GL_TRIANGLES, 0, triangles * 3); + + f->glDisableVertexAttribArray(position); +} + +} // namespace + +Viewport::RendererGL::RendererGL(not_null owner) +: _owner(owner) { +} + +void Viewport::RendererGL::init( + not_null widget, + not_null f) { +} + +void Viewport::RendererGL::deinit( + not_null widget, + not_null f) { +} + +void Viewport::RendererGL::resize( + not_null widget, + not_null f, + int w, + int h) { +} + +void Viewport::RendererGL::paint( + not_null widget, + not_null f) { +} + +} // namespace Calls::Group diff --git a/Telegram/SourceFiles/calls/group/calls_group_viewport_opengl.h b/Telegram/SourceFiles/calls/group/calls_group_viewport_opengl.h new file mode 100644 index 0000000000..3539e0cf17 --- /dev/null +++ b/Telegram/SourceFiles/calls/group/calls_group_viewport_opengl.h @@ -0,0 +1,52 @@ +/* +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 "calls/group/calls_group_viewport.h" +#include "ui/gl/gl_surface.h" + +#include +#include + +namespace Calls::Group { + +class Viewport::RendererGL final : public Ui::GL::Renderer { +public: + explicit RendererGL(not_null owner); + + void init( + not_null widget, + not_null f) override; + + void deinit( + not_null widget, + not_null f) override; + + void resize( + not_null widget, + not_null f, + int w, + int h) override; + + void paint( + not_null widget, + not_null f) override; + +private: + const not_null _owner; + + std::optional _frameBuffer; + std::optional _fillBuffer; + std::optional _bgBuffer; + std::optional _frameProgram; + std::optional _fillProgram; + std::optional _bgProgram; + +}; + +} // namespace Calls::Group diff --git a/Telegram/SourceFiles/calls/group/calls_group_viewport_raster.cpp b/Telegram/SourceFiles/calls/group/calls_group_viewport_raster.cpp new file mode 100644 index 0000000000..757af415a0 --- /dev/null +++ b/Telegram/SourceFiles/calls/group/calls_group_viewport_raster.cpp @@ -0,0 +1,221 @@ +/* +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 "calls/group/calls_group_viewport_raster.h" + +#include "calls/group/calls_group_common.h" +#include "calls/group/calls_group_viewport_tile.h" +#include "calls/group/calls_group_members_row.h" +#include "media/view/media_view_pip.h" +#include "webrtc/webrtc_video_track.h" +#include "lang/lang_keys.h" +#include "styles/style_calls.h" +#include "styles/palette.h" + +namespace Calls::Group { +namespace { + +constexpr auto kShadowMaxAlpha = 80; + +} // namespace + +Viewport::Renderer::Renderer(not_null owner) +: _owner(owner) +, _pinIcon(st::groupCallLargeVideoPin) +, _pinBackground( + (st::groupCallLargeVideoWide.pinPadding.top() + + st::groupCallLargeVideoPin.icon.height() + + st::groupCallLargeVideoWide.pinPadding.bottom()) / 2, + st::radialBg) +, _pinTextOn(st::semiboldTextStyle, tr::lng_pinned_unpin(tr::now)) +, _pinTextOff(st::semiboldTextStyle, tr::lng_pinned_pin(tr::now)) { +} + +void Viewport::Renderer::paintFallback( + Painter &&p, + const QRegion &clip, + Ui::GL::Backend backend) { + auto bg = clip; + const auto guard = gsl::finally([&] { + for (const auto rect : bg) { + p.fillRect(rect, st::groupCallBg); + } + }); + + auto hq = PainterHighQualityEnabler(p); + const auto bounding = clip.boundingRect(); + const auto opengl = (backend == Ui::GL::Backend::OpenGL); + for (const auto &tile : _owner->_tiles) { + paintTile(p, tile.get(), bounding, opengl, bg); + } +} + +void Viewport::Renderer::paintTile( + Painter &p, + not_null tile, + const QRect &clip, + bool opengl, + QRegion &bg) { + const auto track = tile->track(); + const auto [image, rotation] = track->frameOriginalWithRotation(); + if (image.isNull()) { + return; + } + + const auto fill = [&](QRect rect) { + const auto intersected = rect.intersected(clip); + if (!intersected.isEmpty()) { + p.fillRect(intersected, st::groupCallMembersBg); + bg -= intersected; + } + }; + + using namespace Media::View; + const auto geometry = tile->geometry(); + const auto x = geometry.x(); + const auto y = geometry.y(); + const auto width = geometry.width(); + const auto height = geometry.height(); + const auto scaled = FlipSizeByRotation( + image.size(), + rotation + ).scaled(QSize(width, height), Qt::KeepAspectRatio); + const auto left = (width - scaled.width()) / 2; + const auto top = (height - scaled.height()) / 2; + const auto target = QRect(QPoint(x + left, y + top), scaled); + if (UsePainterRotation(rotation, opengl)) { + if (rotation) { + p.save(); + p.rotate(rotation); + } + p.drawImage(RotatedRect(target, rotation), image); + if (rotation) { + p.restore(); + } + } else if (rotation) { + p.drawImage(target, RotateFrameImage(image, rotation)); + } else { + p.drawImage(target, image); + } + bg -= target; + track->markFrameShown(); + + if (left > 0) { + fill({ x, y, left, height }); + } + if (const auto right = left + scaled.width(); right < width) { + fill({ x + right, y, width - right, height }); + } + if (top > 0) { + fill({ x, y, width, top }); + } + if (const auto bottom = top + scaled.height(); bottom < height) { + fill({ x, y + bottom, width, height - bottom }); + } + + paintTileControls(p, x, y, width, height, tile); +} + +void Viewport::Renderer::paintTileControls( + Painter &p, + int x, + int y, + int width, + int height, + not_null tile) { + p.setClipRect(x, y, width, height); + const auto guard = gsl::finally([&] { p.setClipping(false); }); + + // Pin. + const auto wide = _owner->wide(); + if (wide) { + const auto inner = tile->pinInner().translated(x, y); + const auto pinned = tile->pinned(); + const auto &icon = st::groupCallLargeVideoPin.icon; + const auto &st = st::groupCallLargeVideoWide; + _pinBackground.paint(p, inner); + _pinIcon.paint( + p, + inner.marginsRemoved(st.pinPadding).topLeft(), + pinned ? 1. : 0.); + p.setPen(st::groupCallVideoTextFg); + const auto &text = (pinned ? _pinTextOn : _pinTextOff); + text.drawLeft( + p, + (inner.x() + + st.pinPadding.left() + + icon.width() + + st.pinTextPosition.x()), + (inner.y() + + st.pinPadding.top() + + st.pinTextPosition.y()), + text.maxWidth(), + _owner->widget()->width()); + } + + const auto &st = wide + ? st::groupCallLargeVideoWide + : st::groupCallLargeVideoNarrow; + const auto fullShift = st.namePosition.y() + st::normalFont->height; + const auto shown = _owner->_controlsShownRatio; + if (shown == 0.) { + return; + } + + const auto shift = anim::interpolate(fullShift, 0, shown); + auto &shadow = wide ? _shadowWide : _shadowNarrow; + // Shadow. + if (shadow.isNull()) { + shadow = GenerateShadow(st.shadowHeight, 0, kShadowMaxAlpha); + } + const auto shadowRect = QRect( + x, + y + (height - anim::interpolate(0, st.shadowHeight, shown)), + width, + st.shadowHeight); + const auto shadowFill = shadowRect.intersected({ x, y, width, height }); + if (shadowFill.isEmpty()) { + return; + } + const auto factor = style::DevicePixelRatio(); + p.drawImage( + shadowFill, + shadow, + QRect( + 0, + (shadowFill.y() - shadowRect.y()) * factor, + shadow.width(), + shadowFill.height() * factor)); + const auto row = tile->row(); + row->lazyInitialize(st::groupCallMembersListItem); + + // Mute. + const auto &icon = st::groupCallLargeVideoCrossLine.icon; + const auto iconLeft = x + width - st.iconPosition.x() - icon.width(); + const auto iconTop = y + (height + - st.iconPosition.y() + - icon.height() + + shift); + row->paintMuteIcon( + p, + { iconLeft, iconTop, icon.width(), icon.height() }, + MembersRowStyle::LargeVideo); + + // Name. + p.setPen(st::groupCallVideoTextFg); + const auto hasWidth = width + - st.iconPosition.x() - icon.width() + - st.namePosition.x(); + const auto nameLeft = x + st.namePosition.x(); + const auto nameTop = y + (height + - st.namePosition.y() + - st::semiboldFont->height + + shift); + row->name().drawLeftElided(p, nameLeft, nameTop, hasWidth, width); +} + +} // namespace Calls::Group diff --git a/Telegram/SourceFiles/calls/group/calls_group_viewport_raster.h b/Telegram/SourceFiles/calls/group/calls_group_viewport_raster.h new file mode 100644 index 0000000000..6a8f35ae5c --- /dev/null +++ b/Telegram/SourceFiles/calls/group/calls_group_viewport_raster.h @@ -0,0 +1,53 @@ +/* +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 "calls/group/calls_group_viewport.h" +#include "ui/round_rect.h" +#include "ui/effects/cross_line.h" +#include "ui/gl/gl_surface.h" +#include "ui/text/text.h" + +namespace Calls::Group { + +class Viewport::Renderer final : public Ui::GL::Renderer { +public: + explicit Renderer(not_null owner); + + void paintFallback( + Painter &&p, + const QRegion &clip, + Ui::GL::Backend backend) override; + +private: + void paintTile( + Painter &p, + not_null tile, + const QRect &clip, + bool opengl, + QRegion &bg); + void paintTileControls( + Painter &p, + int x, + int y, + int width, + int height, + not_null tile); + + const not_null _owner; + + QImage _shadowWide; + QImage _shadowNarrow; + Ui::CrossLineAnimation _pinIcon; + Ui::RoundRect _pinBackground; + Ui::Text::String _pinTextOn; + Ui::Text::String _pinTextOff; + +}; + +} // namespace Calls::Group diff --git a/Telegram/SourceFiles/calls/group/calls_group_viewport_tile.cpp b/Telegram/SourceFiles/calls/group/calls_group_viewport_tile.cpp new file mode 100644 index 0000000000..4610b9133d --- /dev/null +++ b/Telegram/SourceFiles/calls/group/calls_group_viewport_tile.cpp @@ -0,0 +1,126 @@ +/* +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 "calls/group/calls_group_viewport_tile.h" + +#include "webrtc/webrtc_video_track.h" +#include "lang/lang_keys.h" +#include "styles/style_calls.h" + +namespace Calls::Group { + +Viewport::VideoTile::VideoTile( + const VideoEndpoint &endpoint, + LargeVideoTrack track, + rpl::producer pinned, + Fn update) +: _endpoint(endpoint) +, _update(std::move(update)) +, _track(track) { + Expects(track.track != nullptr); + Expects(track.row != nullptr); + + setup(std::move(pinned)); +} + +QRect Viewport::VideoTile::pinInner() const { + return _pinInner.translated(0, -pinSlide()); +} + +QRect Viewport::VideoTile::pinOuter() const { + return _pinOuter; +} + +int Viewport::VideoTile::pinSlide() const { + return anim::interpolate( + st::groupCallLargeVideoWide.pinPosition.y() + _pinInner.height(), + 0, + _pinShownAnimation.value(_pinShown ? 1. : 0.)); +} + +void Viewport::VideoTile::setGeometry(QRect geometry) { + _geometry = geometry; + updatePinnedGeometry(); +} + +void Viewport::VideoTile::togglePinShown(bool shown) { + if (_pinShown == shown) { + return; + } + _pinShown = shown; + _pinShownAnimation.start( + _update, + shown ? 0. : 1., + shown ? 1. : 0., + st::slideWrapDuration); +} + +bool Viewport::VideoTile::updateRequestedQuality(VideoQuality quality) { + if (!_quality || *_quality == quality) { + return false; + } + _quality = quality; + return true; +} + +void Viewport::VideoTile::updatePinnedGeometry() { + const auto &st = st::groupCallLargeVideoWide; + const auto &icon = st::groupCallLargeVideoPin.icon; + const auto innerWidth = icon.width() + + st.pinTextPosition.x() + + st::semiboldFont->width(_pinned + ? tr::lng_pinned_unpin(tr::now) + : tr::lng_pinned_pin(tr::now)); + const auto innerHeight = icon.height(); + const auto buttonWidth = st.pinPadding.left() + + innerWidth + + st.pinPadding.right(); + const auto buttonHeight = st.pinPadding.top() + + innerHeight + + st.pinPadding.bottom(); + const auto fullWidth = st.pinPosition.x() * 2 + buttonWidth; + const auto fullHeight = st.pinPosition.y() * 2 + buttonHeight; + _pinInner = QRect( + _geometry.width() - st.pinPosition.x() - buttonWidth, + st.pinPosition.y(), + buttonWidth, + buttonHeight); + _pinOuter = QRect( + _geometry.width() - fullWidth, + 0, + fullWidth, + fullHeight); +} + +void Viewport::VideoTile::setup(rpl::producer pinned) { + std::move( + pinned + ) | rpl::filter([=](bool pinned) { + return (_pinned != pinned); + }) | rpl::start_with_next([=](bool pinned) { + _pinned = pinned; + updatePinnedGeometry(); + _update(); + }, _lifetime); + + _track.track->renderNextFrame( + ) | rpl::start_with_next([=] { + const auto size = _track.track->frameSize(); + if (size.isEmpty()) { + _track.track->markFrameShown(); + } else { + _trackSize = size; + } + _update(); + }, _lifetime); + + if (const auto size = _track.track->frameSize(); !size.isEmpty()) { + _trackSize = size; + } +} + +} // namespace Calls::Group diff --git a/Telegram/SourceFiles/calls/group/calls_group_viewport_tile.h b/Telegram/SourceFiles/calls/group/calls_group_viewport_tile.h new file mode 100644 index 0000000000..a09867c085 --- /dev/null +++ b/Telegram/SourceFiles/calls/group/calls_group_viewport_tile.h @@ -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 "calls/group/calls_group_viewport.h" +#include "calls/group/calls_group_call.h" +#include "calls/group/calls_group_large_video.h" // LargeVideoTrack. + +#include "ui/effects/animations.h" +#include "ui/effects/cross_line.h" +#include "ui/round_rect.h" + +namespace Calls::Group { + +class Viewport::VideoTile final { +public: + VideoTile( + const VideoEndpoint &endpoint, + LargeVideoTrack track, + rpl::producer pinned, + Fn update); + + [[nodiscard]] not_null track() const { + return _track.track; + } + [[nodiscard]] not_null row() const { + return _track.row; + } + [[nodiscard]] QRect geometry() const { + return _geometry; + } + [[nodiscard]] bool pinned() const { + return _pinned; + } + [[nodiscard]] QRect pinOuter() const; + [[nodiscard]] QRect pinInner() const; + [[nodiscard]] const VideoEndpoint &endpoint() const { + return _endpoint; + } + [[nodiscard]] QSize trackSize() const { + return _trackSize.current(); + } + [[nodiscard]] rpl::producer trackSizeValue() const { + return _trackSize.value(); + } + + void setGeometry(QRect geometry); + void togglePinShown(bool shown); + bool updateRequestedQuality(VideoQuality quality); + + [[nodiscard]] rpl::lifetime &lifetime() { + return _lifetime; + } + +private: + void setup(rpl::producer pinned); + [[nodiscard]] int pinSlide() const; + void updatePinnedGeometry(); + + const VideoEndpoint _endpoint; + const Fn _update; + + LargeVideoTrack _track; + QRect _geometry; + rpl::variable _trackSize; + QRect _pinOuter; + QRect _pinInner; + Ui::Animations::Simple _pinShownAnimation; + bool _pinShown = false; + bool _pinned = false; + std::optional _quality; + + rpl::lifetime _lifetime; + +}; + +} // namespace Calls::Group