diff --git a/Telegram/Resources/icons/calls/video_tooltip.png b/Telegram/Resources/icons/calls/video_tooltip.png new file mode 100644 index 000000000..60ecf2cd7 Binary files /dev/null and b/Telegram/Resources/icons/calls/video_tooltip.png differ diff --git a/Telegram/Resources/icons/calls/video_tooltip@2x.png b/Telegram/Resources/icons/calls/video_tooltip@2x.png new file mode 100644 index 000000000..37aee8b75 Binary files /dev/null and b/Telegram/Resources/icons/calls/video_tooltip@2x.png differ diff --git a/Telegram/Resources/icons/calls/video_tooltip@3x.png b/Telegram/Resources/icons/calls/video_tooltip@3x.png new file mode 100644 index 000000000..13a66723a Binary files /dev/null and b/Telegram/Resources/icons/calls/video_tooltip@3x.png differ diff --git a/Telegram/SourceFiles/calls/calls.style b/Telegram/SourceFiles/calls/calls.style index 6e8c20db7..9a5e9584f 100644 --- a/Telegram/SourceFiles/calls/calls.style +++ b/Telegram/SourceFiles/calls/calls.style @@ -1251,7 +1251,7 @@ groupCallTooltip: Tooltip(defaultTooltip) { } groupCallNiceTooltip: ImportantTooltip(defaultImportantTooltip) { bg: importantTooltipBg; - padding: margins(10px, 3px, 10px, 5px); + padding: margins(10px, 1px, 10px, 3px); radius: 4px; arrow: 4px; } @@ -1262,5 +1262,16 @@ groupCallNiceTooltipLabel: FlatLabel(defaultImportantTooltipLabel) { linkFontOver: font(11px underline); } } +groupCallStickedTooltip: ImportantTooltip(groupCallNiceTooltip) { + padding: margins(10px, 1px, 6px, 3px); +} +groupCallStickedTooltipClose: IconButton(defaultIconButton) { + width: 20px; + height: 20px; + iconPosition: point(4px, 3px); + icon: icon {{ "calls/video_tooltip", importantTooltipFg }}; + iconOver: icon {{ "calls/video_tooltip", importantTooltipFg }}; + ripple: emptyRippleAnimation; +} groupCallNiceTooltipTop: 4px; groupCallPaused: icon {{ "calls/video_large_paused", groupCallVideoTextFg }}; diff --git a/Telegram/SourceFiles/calls/group/calls_group_call.cpp b/Telegram/SourceFiles/calls/group/calls_group_call.cpp index 83a99fd2d..ebe8d608d 100644 --- a/Telegram/SourceFiles/calls/group/calls_group_call.cpp +++ b/Telegram/SourceFiles/calls/group/calls_group_call.cpp @@ -187,6 +187,28 @@ struct GroupCall::SinkPointer { std::weak_ptr data; }; +struct GroupCall::VideoTrack { + VideoTrack(bool paused, bool requireARGB32, not_null peer); + + Webrtc::VideoTrack track; + rpl::variable trackSize; + not_null peer; + rpl::lifetime lifetime; + Group::VideoQuality quality = Group::VideoQuality(); + bool shown = false; +}; + +GroupCall::VideoTrack::VideoTrack( + bool paused, + bool requireARGB32, + not_null peer) +: track((paused + ? Webrtc::VideoState::Paused + : Webrtc::VideoState::Active), + requireARGB32) +, peer(peer) { +} + [[nodiscard]] bool IsGroupCallAdmin( not_null peer, not_null participantPeer) { @@ -451,6 +473,21 @@ void GroupCall::MediaChannelDescriptionsTask::cancel() { } } +not_null GroupCall::TrackPeer( + const std::unique_ptr &track) { + return track->peer; +} + +not_null GroupCall::TrackPointer( + const std::unique_ptr &track) { + return &track->track; +} + +rpl::producer GroupCall::TrackSizeValue( + const std::unique_ptr &track) { + return track->trackSize.value(); +} + GroupCall::GroupCall( not_null delegate, Group::JoinInfo info, @@ -1064,43 +1101,39 @@ void GroupCall::markEndpointActive( if (active) { const auto i = _activeVideoTracks.emplace( endpoint, - VideoTrack{ - .track = std::make_unique( - (paused - ? Webrtc::VideoState::Paused - : Webrtc::VideoState::Active), - _requireARGB32), - .peer = endpoint.peer, - }).first; - const auto track = i->second.track.get(); + std::make_unique( + paused, + _requireARGB32, + endpoint.peer)).first; + const auto track = &i->second->track; track->renderNextFrame( ) | rpl::start_with_next([=] { - auto &activeTrack = _activeVideoTracks[endpoint]; + const auto activeTrack = _activeVideoTracks[endpoint].get(); const auto size = track->frameSize(); if (size.isEmpty()) { track->markFrameShown(); - } else if (!activeTrack.shown) { - activeTrack.shown = true; + } else if (!activeTrack->shown) { + activeTrack->shown = true; markTrackShown(endpoint, true); } - activeTrack.trackSize = size; - }, i->second.lifetime); + activeTrack->trackSize = size; + }, i->second->lifetime); const auto size = track->frameSize(); - i->second.trackSize = size; + i->second->trackSize = size; if (!size.isEmpty() || paused) { - i->second.shown = true; + i->second->shown = true; shown = true; } else { track->stateValue( ) | rpl::filter([=](Webrtc::VideoState state) { return (state == Webrtc::VideoState::Paused) - && !_activeVideoTracks[endpoint].shown; + && !_activeVideoTracks[endpoint]->shown; }) | rpl::start_with_next([=] { - _activeVideoTracks[endpoint].shown = true; + _activeVideoTracks[endpoint]->shown = true; markTrackShown(endpoint, true); - }, i->second.lifetime); + }, i->second->lifetime); } addVideoOutput(i->first.id, { track->sink() }); } else { @@ -1144,7 +1177,7 @@ void GroupCall::markTrackPaused(const VideoEndpoint &endpoint, bool paused) { const auto i = _activeVideoTracks.find(endpoint); Assert(i != end(_activeVideoTracks)); - i->second.track->setState(paused + i->second->track.setState(paused ? Webrtc::VideoState::Paused : Webrtc::VideoState::Active); } @@ -2420,13 +2453,13 @@ void GroupCall::updateRequestedVideoChannels() { .ssrcGroups = (params->camera.endpointId == endpointId ? params->camera.ssrcGroups : params->screen.ssrcGroups), - .minQuality = ((video.quality == Group::VideoQuality::Full + .minQuality = ((video->quality == Group::VideoQuality::Full && endpoint.type == VideoEndpointType::Screen) ? Quality::Full : Quality::Thumbnail), - .maxQuality = ((video.quality == Group::VideoQuality::Full) + .maxQuality = ((video->quality == Group::VideoQuality::Full) ? Quality::Full - : (video.quality == Group::VideoQuality::Medium + : (video->quality == Group::VideoQuality::Medium && endpoint.type != VideoEndpointType::Screen) ? Quality::Medium : Quality::Thumbnail), @@ -2911,10 +2944,10 @@ void GroupCall::requestVideoQuality( return; } const auto i = _activeVideoTracks.find(endpoint); - if (i == end(_activeVideoTracks) || i->second.quality == quality) { + if (i == end(_activeVideoTracks) || i->second->quality == quality) { return; } - i->second.quality = quality; + i->second->quality = quality; updateRequestedVideoChannelsDelayed(); } diff --git a/Telegram/SourceFiles/calls/group/calls_group_call.h b/Telegram/SourceFiles/calls/group/calls_group_call.h index e6bcae38e..6a4611050 100644 --- a/Telegram/SourceFiles/calls/group/calls_group_call.h +++ b/Telegram/SourceFiles/calls/group/calls_group_call.h @@ -98,6 +98,8 @@ struct VideoEndpoint { std::string id; [[nodiscard]] bool empty() const noexcept { + Expects(id.empty() || peer != nullptr); + return id.empty(); } [[nodiscard]] explicit operator bool() const noexcept { @@ -194,6 +196,15 @@ public: using GlobalShortcutManager = base::GlobalShortcutManager; + struct VideoTrack; + + [[nodiscard]] static not_null TrackPeer( + const std::unique_ptr &track); + [[nodiscard]] static not_null TrackPointer( + const std::unique_ptr &track); + [[nodiscard]] static rpl::producer TrackSizeValue( + const std::unique_ptr &track); + GroupCall( not_null delegate, Group::JoinInfo info, @@ -321,27 +332,8 @@ public: -> rpl::producer { return _videoEndpointLarge.value(); } - - struct VideoTrack { - std::unique_ptr track; - rpl::variable trackSize; - PeerData *peer = nullptr; - rpl::lifetime lifetime; - Group::VideoQuality quality = Group::VideoQuality(); - bool shown = false; - - [[nodiscard]] explicit operator bool() const { - return (track != nullptr); - } - [[nodiscard]] bool operator==(const VideoTrack &other) const { - return (track == other.track) && (peer == other.peer); - } - [[nodiscard]] bool operator!=(const VideoTrack &other) const { - return !(*this == other); - } - }; [[nodiscard]] auto activeVideoTracks() const - -> const base::flat_map & { + -> const base::flat_map> & { return _activeVideoTracks; } [[nodiscard]] auto shownVideoTracks() const @@ -625,7 +617,9 @@ private: rpl::event_stream _videoStreamActiveUpdates; rpl::event_stream _videoStreamPausedUpdates; rpl::event_stream _videoStreamShownUpdates; - base::flat_map _activeVideoTracks; + base::flat_map< + VideoEndpoint, + std::unique_ptr> _activeVideoTracks; base::flat_set _shownVideoTracks; rpl::variable _videoEndpointLarge; rpl::variable _videoEndpointPinned = false; diff --git a/Telegram/SourceFiles/calls/group/calls_group_panel.cpp b/Telegram/SourceFiles/calls/group/calls_group_panel.cpp index 03b46c3e8..7bd3589b5 100644 --- a/Telegram/SourceFiles/calls/group/calls_group_panel.cpp +++ b/Telegram/SourceFiles/calls/group/calls_group_panel.cpp @@ -499,17 +499,26 @@ void Panel::refreshVideoButtons(std::optional overrideWideMode) { &st::groupCallVideoActiveSmall); _video->show(); _video->setClickedCallback([=] { + hideStickedTooltip( + StickedTooltip::Camera, + StickedTooltipHide::Activated); _call->toggleVideo(!_call->isSharingCamera()); }); _video->setColorOverrides( toggleableOverrides(_call->isSharingCameraValue())); _call->isSharingCameraValue( ) | rpl::start_with_next([=](bool sharing) { + if (sharing) { + hideStickedTooltip( + StickedTooltip::Camera, + StickedTooltipHide::Activated); + } _video->setProgress(sharing ? 1. : 0.); }, _video->lifetime()); _call->mutedValue( ) | rpl::start_with_next([=] { updateButtonsGeometry(); + showStickedTooltip(); }, _video->lifetime()); } if (!_screenShare) { @@ -536,6 +545,45 @@ void Panel::refreshVideoButtons(std::optional overrideWideMode) { updateButtonsGeometry(); } +void Panel::hideStickedTooltip(StickedTooltipHide hide) { + if (!_stickedTooltipClose || !_niceTooltipControl) { + return; + } + if (_niceTooltipControl.data() == _video.data()) { + hideStickedTooltip(StickedTooltip::Camera, hide); + } else if (_niceTooltipControl.data() == _mute->outer().get()) { + hideStickedTooltip(StickedTooltip::Microphone, hide); + } +} + +void Panel::hideStickedTooltip( + StickedTooltip type, + StickedTooltipHide hide) { + if (hide != StickedTooltipHide::Unavailable) { + _stickedTooltipsShown |= type; + if (hide == StickedTooltipHide::Discarded) { + // #TODO calls save to settings. + } + } + const auto control = (type == StickedTooltip::Camera) + ? _video.data() + : (type == StickedTooltip::Microphone) + ? _mute->outer().get() + : nullptr; + if (_niceTooltipControl.data() == control) { + hideNiceTooltip(); + } +} + +void Panel::hideNiceTooltip() { + if (!_niceTooltip) { + return; + } + _stickedTooltipClose = nullptr; + _niceTooltip.release()->toggleAnimated(false); + _niceTooltipControl = nullptr; +} + void Panel::initShareAction() { const auto showBoxCallback = [=](object_ptr next) { _layerBg->showBox(std::move(next)); @@ -843,9 +891,9 @@ void Panel::raiseControls() { void Panel::setupVideo(not_null viewport) { const auto setupTile = [=]( const VideoEndpoint &endpoint, - const GroupCall::VideoTrack &track) { + const std::unique_ptr &track) { using namespace rpl::mappers; - const auto row = _members->lookupRow(track.peer); + const auto row = _members->lookupRow(GroupCall::TrackPeer(track)); Assert(row != nullptr); auto pinned = rpl::combine( _call->videoEndpointLargeValue(), @@ -853,8 +901,8 @@ void Panel::setupVideo(not_null viewport) { ) | rpl::map(_1 == endpoint && _2); viewport->add( endpoint, - VideoTileTrack{ track.track.get(), row }, - track.trackSize.value(), + VideoTileTrack{ GroupCall::TrackPointer(track), row }, + GroupCall::TrackSizeValue(track), std::move(pinned)); }; for (const auto &[endpoint, track] : _call->activeVideoTracks()) { @@ -908,18 +956,24 @@ void Panel::toggleWideControls(bool shown) { } _showWideControls = shown; crl::on_main(widget(), [=] { - if (_wideControlsShown == _showWideControls) { - return; - } - _wideControlsShown = _showWideControls; - _wideControlsAnimation.start( - [=] { updateButtonsGeometry(); }, - _wideControlsShown ? 0. : 1., - _wideControlsShown ? 1. : 0., - st::slideWrapDuration); + updateWideControlsVisibility(); }); } +void Panel::updateWideControlsVisibility() { + const auto shown = _showWideControls + || (_stickedTooltipClose != nullptr); + if (_wideControlsShown == shown) { + return; + } + _wideControlsShown = shown; + _wideControlsAnimation.start( + [=] { updateButtonsGeometry(); }, + _wideControlsShown ? 0. : 1., + _wideControlsShown ? 1. : 0., + st::slideWrapDuration); +} + void Panel::subscribeToChanges(not_null real) { const auto validateRecordingMark = [=](bool recording) { if (!recording && _recordingMark) { @@ -988,6 +1042,7 @@ void Panel::subscribeToChanges(not_null real) { _call->isSharingCameraValue() ) | rpl::start_with_next([=] { refreshVideoButtons(); + showStickedTooltip(); }, widget()->lifetime()); rpl::combine( @@ -1373,6 +1428,7 @@ bool Panel::updateMode() { updateButtonsStyles(); refreshControlsBackground(); updateControlsGeometry(); + showStickedTooltip(); return true; } @@ -1557,8 +1613,12 @@ void Panel::trackControl(Ui::RpWidget *widget, rpl::lifetime &lifetime) { } void Panel::trackControlOver(not_null control, bool over) { - if (_niceTooltip) { - _niceTooltip.release()->toggleAnimated(false); + if (_stickedTooltipClose) { + if (!over) { + return; + } + } else { + hideNiceTooltip(); } if (over) { Ui::Integration::Instance().registerLeaveSubscription(control); @@ -1569,7 +1629,37 @@ void Panel::trackControlOver(not_null control, bool over) { toggleWideControls(over); } -void Panel::showNiceTooltip(not_null control) { +void Panel::showStickedTooltip() { + static const auto kHasCamera = !Webrtc::GetVideoInputList().empty(); + if (!(_stickedTooltipsShown & StickedTooltip::Camera) + && (_mode.current() == PanelMode::Wide) + && _video + && _call->videoIsWorking() + && !_call->mutedByAdmin() + && kHasCamera) { // Don't recount this every time for now. + showNiceTooltip(_video, NiceTooltipType::Sticked); + return; + } + hideStickedTooltip( + StickedTooltip::Camera, + StickedTooltipHide::Unavailable); + + if (!(_stickedTooltipsShown & StickedTooltip::Microphone) + && (_mode.current() == PanelMode::Wide) + && _mute + && !_call->mutedByAdmin() + && false) { // Check if there is incoming sound. + showNiceTooltip(_mute->outer(), NiceTooltipType::Sticked); + return; + } + hideStickedTooltip( + StickedTooltip::Microphone, + StickedTooltipHide::Unavailable); +} + +void Panel::showNiceTooltip( + not_null control, + NiceTooltipType type) { auto text = [&]() -> rpl::producer { if (control == _screenShare.data()) { if (_call->mutedByAdmin()) { @@ -1597,38 +1687,95 @@ void Panel::showNiceTooltip(not_null control) { }(); if (!text || _wideControlsAnimation.animating() - || !_wideControlsShown) { + || !_wideControlsShown + || _stickedTooltipClose) { return; } + const auto inner = [&]() -> Ui::RpWidget* { + const auto normal = (type == NiceTooltipType::Normal); + auto container = normal + ? nullptr + : Ui::CreateChild(widget().get()); + const auto label = Ui::CreateChild( + (normal ? widget().get() : container), + std::move(text), + st::groupCallNiceTooltipLabel); + if (normal) { + return label; + } + const auto button = Ui::CreateChild( + container, + st::groupCallStickedTooltipClose); + rpl::combine( + label->sizeValue(), + button->sizeValue() + ) | rpl::start_with_next([=](QSize text, QSize close) { + const auto height = std::max(text.height(), close.height()); + container->resize(text.width() + close.width(), height); + label->move(0, (height - text.height()) / 2); + button->move(text.width(), (height - close.height()) / 2); + }, container->lifetime()); + button->setClickedCallback([=] { + hideStickedTooltip(StickedTooltipHide::Discarded); + }); + _stickedTooltipClose = button; + updateWideControlsVisibility(); + return container; + }(); _niceTooltip.create( widget().get(), - object_ptr( - widget().get(), - std::move(text), - st::groupCallNiceTooltipLabel), - st::groupCallNiceTooltip); + object_ptr::fromRaw(inner), + (type == NiceTooltipType::Sticked + ? st::groupCallStickedTooltip + : st::groupCallNiceTooltip)); const auto tooltip = _niceTooltip.data(); const auto weak = QPointer(tooltip); const auto destroy = [=] { delete weak.data(); }; - tooltip->setAttribute(Qt::WA_TransparentForMouseEvents); + if (type != NiceTooltipType::Sticked) { + tooltip->setAttribute(Qt::WA_TransparentForMouseEvents); + } tooltip->setHiddenCallback(destroy); base::qt_signal_producer( control.get(), &QObject::destroyed ) | rpl::start_with_next(destroy, tooltip->lifetime()); - const auto geometry = control->geometry(); + _niceTooltipControl = control; + updateTooltipGeometry(); + tooltip->toggleAnimated(true); +} + +void Panel::updateTooltipGeometry() { + if (!_niceTooltip) { + return; + } else if (!_niceTooltipControl) { + hideNiceTooltip(); + return; + } + const auto geometry = _niceTooltipControl->geometry(); + const auto weak = QPointer(_niceTooltip); const auto countPosition = [=](QSize size) { const auto strong = weak.data(); - if (!strong) { - return QPoint(); - } + const auto wide = (_mode.current() == PanelMode::Wide); const auto top = geometry.y() - - st::groupCallNiceTooltipTop + - (wide ? st::groupCallNiceTooltipTop : 0) - size.height(); const auto middle = geometry.center().x(); + if (!strong) { + return QPoint(); + } else if (!wide) { + return QPoint( + std::max( + std::min( + middle - size.width() / 2, + (widget()->width() + - st::groupCallMembersMargin.right() + - size.width())), + st::groupCallMembersMargin.left()), + top); + } const auto back = _controlsBackgroundWide.data(); if (size.width() >= _viewport->widget()->width()) { return QPoint(_viewport->widget()->x(), top); @@ -1645,8 +1792,7 @@ void Panel::showNiceTooltip(not_null control) { return QPoint(middle - size.width() / 2, top); } }; - tooltip->pointAt(geometry, RectPart::Top, countPosition); - tooltip->toggleAnimated(true); + _niceTooltip->pointAt(geometry, RectPart::Top, countPosition); } void Panel::trackControls(bool track) { @@ -1832,6 +1978,7 @@ void Panel::updateButtonsGeometry() { width, st::groupCallMembersBottomSkip); } + updateTooltipGeometry(); } bool Panel::videoButtonInNarrowMode() const { diff --git a/Telegram/SourceFiles/calls/group/calls_group_panel.h b/Telegram/SourceFiles/calls/group/calls_group_panel.h index 4383b510c..4e2a7cc49 100644 --- a/Telegram/SourceFiles/calls/group/calls_group_panel.h +++ b/Telegram/SourceFiles/calls/group/calls_group_panel.h @@ -84,6 +84,24 @@ private: using State = GroupCall::State; struct ControlsBackgroundNarrow; + enum class NiceTooltipType { + Normal, + Sticked, + }; + enum class StickedTooltip { + Camera = 0x01, + Microphone = 0x02, + }; + friend constexpr inline bool is_flag_type(StickedTooltip) { + return true; + }; + using StickedTooltips = base::flags; + enum class StickedTooltipHide { + Unavailable, + Activated, + Discarded, + }; + std::unique_ptr createWindow(); [[nodiscard]] not_null widget() const; @@ -111,11 +129,18 @@ private: void trackControl(Ui::RpWidget *widget, rpl::lifetime &lifetime); void trackControlOver(not_null control, bool over); - void showNiceTooltip(not_null control); + void showNiceTooltip( + not_null control, + NiceTooltipType type = NiceTooltipType::Normal); + void showStickedTooltip(); + void hideStickedTooltip(StickedTooltipHide hide); + void hideStickedTooltip(StickedTooltip type, StickedTooltipHide hide); + void hideNiceTooltip(); bool updateMode(); void updateControlsGeometry(); void updateButtonsGeometry(); + void updateTooltipGeometry(); void updateButtonsStyles(); void updateMembersGeometry(); void refreshControlsBackground(); @@ -127,6 +152,7 @@ private: std::optional overrideWideMode = std::nullopt); void refreshTopButton(); void toggleWideControls(bool shown); + void updateWideControlsVisibility(); [[nodiscard]] bool videoButtonInNarrowMode() const; void endCall(); @@ -202,6 +228,9 @@ private: std::unique_ptr _mute; object_ptr _hangup; object_ptr _niceTooltip = { nullptr }; + QPointer _stickedTooltipClose; + QPointer _niceTooltipControl; + StickedTooltips _stickedTooltipsShown; Fn _callShareLinkCallback; const std::unique_ptr _toasts; diff --git a/Telegram/lib_ui b/Telegram/lib_ui index d8abc6024..825ef11f1 160000 --- a/Telegram/lib_ui +++ b/Telegram/lib_ui @@ -1 +1 @@ -Subproject commit d8abc60245236e388206e2021bfeb58e83e504fc +Subproject commit 825ef11f1a5c6b00cd571c24525c84dca431eaa2