mirror of
https://github.com/AyuGram/AyuGramDesktop.git
synced 2025-04-16 06:07:06 +02:00
Track peer together with video endpoint.
This commit is contained in:
parent
909a3cef9b
commit
8001efe6ab
3 changed files with 143 additions and 84 deletions
|
@ -563,7 +563,7 @@ void GroupCall::subscribeToReal(not_null<Data::GroupCall*> real) {
|
|||
: endpoint;
|
||||
};
|
||||
const auto guard = gsl::finally([&] {
|
||||
if (newLarge.empty()) {
|
||||
if (!newLarge) {
|
||||
newLarge = chooseLargeVideoEndpoint();
|
||||
}
|
||||
if (_videoEndpointLarge.current() != newLarge) {
|
||||
|
@ -577,6 +577,7 @@ void GroupCall::subscribeToReal(not_null<Data::GroupCall*> real) {
|
|||
}
|
||||
});
|
||||
|
||||
const auto peer = data.was ? data.was->peer : data.now->peer;
|
||||
const auto &wasCameraEndpoint = (data.was && data.was->videoParams)
|
||||
? regularEndpoint(data.was->videoParams->camera.endpoint)
|
||||
: EmptyString();
|
||||
|
@ -595,8 +596,9 @@ void GroupCall::subscribeToReal(not_null<Data::GroupCall*> real) {
|
|||
&& _activeVideoEndpoints.remove(wasCameraEndpoint)
|
||||
&& _incomingVideoEndpoints.contains(wasCameraEndpoint)) {
|
||||
updateCameraNotStreams = wasCameraEndpoint;
|
||||
if (newLarge == wasCameraEndpoint) {
|
||||
_videoEndpointPinned = newLarge = std::string();
|
||||
if (newLarge.endpoint == wasCameraEndpoint) {
|
||||
newLarge = VideoEndpoint();
|
||||
_videoEndpointPinned = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -618,8 +620,9 @@ void GroupCall::subscribeToReal(not_null<Data::GroupCall*> real) {
|
|||
&& _activeVideoEndpoints.remove(wasScreenEndpoint)
|
||||
&& _incomingVideoEndpoints.contains(wasScreenEndpoint)) {
|
||||
updateScreenNotStreams = wasScreenEndpoint;
|
||||
if (newLarge == wasScreenEndpoint) {
|
||||
_videoEndpointPinned = newLarge = std::string();
|
||||
if (newLarge.endpoint == wasScreenEndpoint) {
|
||||
newLarge = VideoEndpoint();
|
||||
_videoEndpointPinned = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -629,62 +632,67 @@ void GroupCall::subscribeToReal(not_null<Data::GroupCall*> real) {
|
|||
const auto wasSounding = data.was && data.was->sounding;
|
||||
if (nowSpeaking == wasSpeaking && nowSounding == wasSounding) {
|
||||
return;
|
||||
} else if (!_videoEndpointPinned.current().empty()) {
|
||||
} else if (_videoEndpointPinned.current()) {
|
||||
return;
|
||||
}
|
||||
if (nowScreenEndpoint != newLarge
|
||||
if (nowScreenEndpoint != newLarge.endpoint
|
||||
&& streamsVideo(nowScreenEndpoint)
|
||||
&& activeVideoEndpointType(newLarge) != EndpointType::Screen) {
|
||||
newLarge = nowScreenEndpoint;
|
||||
&& (activeVideoEndpointType(newLarge.endpoint)
|
||||
!= EndpointType::Screen)) {
|
||||
newLarge = { peer, nowScreenEndpoint };
|
||||
}
|
||||
const auto &participants = real->participants();
|
||||
if (!nowSpeaking
|
||||
&& (wasSpeaking || wasSounding)
|
||||
&& (wasCameraEndpoint == newLarge)) {
|
||||
auto screenEndpoint = std::string();
|
||||
auto speakingEndpoint = std::string();
|
||||
auto soundingEndpoint = std::string();
|
||||
&& (wasCameraEndpoint == newLarge.endpoint)) {
|
||||
auto screenEndpoint = VideoEndpoint();
|
||||
auto speakingEndpoint = VideoEndpoint();
|
||||
auto soundingEndpoint = VideoEndpoint();
|
||||
for (const auto &participant : participants) {
|
||||
const auto params = participant.videoParams.get();
|
||||
if (!params) {
|
||||
continue;
|
||||
}
|
||||
const auto peer = participant.peer;
|
||||
if (streamsVideo(params->screen.endpoint)) {
|
||||
screenEndpoint = params->screen.endpoint;
|
||||
screenEndpoint = { peer, params->screen.endpoint };
|
||||
break;
|
||||
} else if (participant.speaking
|
||||
&& speakingEndpoint.empty()) {
|
||||
&& !speakingEndpoint) {
|
||||
if (streamsVideo(params->camera.endpoint)) {
|
||||
speakingEndpoint = params->camera.endpoint;
|
||||
speakingEndpoint = { peer, params->camera.endpoint };
|
||||
}
|
||||
} else if (!nowSounding
|
||||
&& participant.sounding
|
||||
&& soundingEndpoint.empty()) {
|
||||
&& !soundingEndpoint) {
|
||||
if (streamsVideo(params->camera.endpoint)) {
|
||||
soundingEndpoint = params->camera.endpoint;
|
||||
soundingEndpoint = { peer, params->camera.endpoint };
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!screenEndpoint.empty()) {
|
||||
if (screenEndpoint) {
|
||||
newLarge = screenEndpoint;
|
||||
} else if (!speakingEndpoint.empty()) {
|
||||
} else if (speakingEndpoint) {
|
||||
newLarge = speakingEndpoint;
|
||||
} else if (!soundingEndpoint.empty()) {
|
||||
} else if (soundingEndpoint) {
|
||||
newLarge = soundingEndpoint;
|
||||
}
|
||||
} else if ((nowSpeaking || nowSounding)
|
||||
&& (nowCameraEndpoint != newLarge)
|
||||
&& (activeVideoEndpointType(newLarge) != EndpointType::Screen)
|
||||
&& (nowCameraEndpoint != newLarge.endpoint)
|
||||
&& (activeVideoEndpointType(newLarge.endpoint)
|
||||
!= EndpointType::Screen)
|
||||
&& streamsVideo(nowCameraEndpoint)) {
|
||||
const auto participant = real->participantByEndpoint(newLarge);
|
||||
const auto participant = real->participantByEndpoint(
|
||||
newLarge.endpoint);
|
||||
const auto screen = participant
|
||||
&& (participant->videoParams->screen.endpoint == newLarge);
|
||||
&& (participant->videoParams->screen.endpoint
|
||||
== newLarge.endpoint);
|
||||
const auto speaking = participant && participant->speaking;
|
||||
const auto sounding = participant && participant->sounding;
|
||||
if (!screen
|
||||
&& ((nowSpeaking && !speaking)
|
||||
|| (nowSounding && !sounding))) {
|
||||
newLarge = nowCameraEndpoint;
|
||||
newLarge = { peer, nowCameraEndpoint };
|
||||
}
|
||||
}
|
||||
}, _lifetime);
|
||||
|
@ -878,8 +886,8 @@ void GroupCall::setMyEndpointType(
|
|||
const auto was2 = _activeVideoEndpoints.remove(endpoint);
|
||||
if (was1 && was2) {
|
||||
auto newLarge = _videoEndpointLarge.current();
|
||||
if (newLarge == endpoint) {
|
||||
_videoEndpointPinned = std::string();
|
||||
if (newLarge.endpoint == endpoint) {
|
||||
_videoEndpointPinned = false;
|
||||
_videoEndpointLarge = chooseLargeVideoEndpoint();
|
||||
}
|
||||
_streamsVideoUpdated.fire({ endpoint, false });
|
||||
|
@ -893,13 +901,13 @@ void GroupCall::setMyEndpointType(
|
|||
_streamsVideoUpdated.fire({ endpoint, true });
|
||||
}
|
||||
const auto nowLarge = activeVideoEndpointType(
|
||||
_videoEndpointLarge.current());
|
||||
if (_videoEndpointPinned.current().empty()
|
||||
_videoEndpointLarge.current().endpoint);
|
||||
if (!_videoEndpointPinned.current()
|
||||
&& ((type == EndpointType::Screen
|
||||
&& nowLarge != EndpointType::Screen)
|
||||
|| (type == EndpointType::Camera
|
||||
&& nowLarge == EndpointType::None))) {
|
||||
_videoEndpointLarge = endpoint;
|
||||
_videoEndpointLarge = VideoEndpoint{ _joinAs, endpoint };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1837,8 +1845,8 @@ void GroupCall::ensureControllerCreated() {
|
|||
std::move(descriptor));
|
||||
|
||||
_videoEndpointLarge.changes(
|
||||
) | rpl::start_with_next([=](const std::string &endpoint) {
|
||||
_instance->setFullSizeVideoEndpointId(endpoint);
|
||||
) | rpl::start_with_next([=](const VideoEndpoint &endpoint) {
|
||||
_instance->setFullSizeVideoEndpointId(endpoint.endpoint);
|
||||
_videoLargeTrack = nullptr;
|
||||
_videoLargeTrackWrap = nullptr;
|
||||
if (endpoint.empty()) {
|
||||
|
@ -1850,7 +1858,7 @@ void GroupCall::ensureControllerCreated() {
|
|||
}
|
||||
_videoLargeTrackWrap->sink = Webrtc::CreateProxySink(
|
||||
_videoLargeTrackWrap->track.sink());
|
||||
addVideoOutput(endpoint, { _videoLargeTrackWrap->sink });
|
||||
addVideoOutput(endpoint.endpoint, { _videoLargeTrackWrap->sink });
|
||||
}, _lifetime);
|
||||
|
||||
updateInstanceMuteState();
|
||||
|
@ -2063,7 +2071,7 @@ void GroupCall::setIncomingVideoEndpoints(
|
|||
const auto feedOne = [&](const std::string &endpoint) {
|
||||
if (endpoint.empty()) {
|
||||
return;
|
||||
} else if (endpoint == newLarge) {
|
||||
} else if (endpoint == newLarge.endpoint) {
|
||||
newLargeFound = true;
|
||||
}
|
||||
if (!removed.remove(endpoint)) {
|
||||
|
@ -2080,8 +2088,9 @@ void GroupCall::setIncomingVideoEndpoints(
|
|||
}
|
||||
feedOne(cameraSharingEndpoint());
|
||||
feedOne(screenSharingEndpoint());
|
||||
if (!newLarge.empty() && !newLargeFound) {
|
||||
_videoEndpointPinned = newLarge = std::string();
|
||||
if (newLarge && !newLargeFound) {
|
||||
_videoEndpointPinned = false;
|
||||
newLarge = VideoEndpoint();
|
||||
}
|
||||
if (newLarge.empty()) {
|
||||
_videoEndpointLarge = chooseLargeVideoEndpoint();
|
||||
|
@ -2106,7 +2115,7 @@ void GroupCall::fillActiveVideoEndpoints() {
|
|||
EndpointType type) {
|
||||
if (endpoint.empty()) {
|
||||
return;
|
||||
} else if (endpoint == newLarge) {
|
||||
} else if (endpoint == newLarge.endpoint) {
|
||||
newLargeFound = true;
|
||||
}
|
||||
if (!removed.remove(endpoint)) {
|
||||
|
@ -2129,9 +2138,10 @@ void GroupCall::fillActiveVideoEndpoints() {
|
|||
feedOne(cameraSharingEndpoint(), EndpointType::Camera);
|
||||
feedOne(screenSharingEndpoint(), EndpointType::Screen);
|
||||
if (!newLarge.empty() && !newLargeFound) {
|
||||
_videoEndpointPinned = newLarge = std::string();
|
||||
_videoEndpointPinned = false;
|
||||
newLarge = VideoEndpoint();
|
||||
}
|
||||
if (newLarge.empty()) {
|
||||
if (!newLarge) {
|
||||
_videoEndpointLarge = chooseLargeVideoEndpoint();
|
||||
}
|
||||
for (const auto &[endpoint, type] : removed) {
|
||||
|
@ -2152,15 +2162,15 @@ GroupCall::EndpointType GroupCall::activeVideoEndpointType(
|
|||
: EndpointType::None;
|
||||
}
|
||||
|
||||
std::string GroupCall::chooseLargeVideoEndpoint() const {
|
||||
VideoEndpoint GroupCall::chooseLargeVideoEndpoint() const {
|
||||
const auto real = lookupReal();
|
||||
if (!real) {
|
||||
return std::string();
|
||||
return VideoEndpoint();
|
||||
}
|
||||
auto anyEndpoint = std::string();
|
||||
auto screenEndpoint = std::string();
|
||||
auto speakingEndpoint = std::string();
|
||||
auto soundingEndpoint = std::string();
|
||||
auto anyEndpoint = VideoEndpoint();
|
||||
auto screenEndpoint = VideoEndpoint();
|
||||
auto speakingEndpoint = VideoEndpoint();
|
||||
auto soundingEndpoint = VideoEndpoint();
|
||||
const auto &myCameraEndpoint = cameraSharingEndpoint();
|
||||
const auto &myScreenEndpoint = screenSharingEndpoint();
|
||||
const auto &participants = real->participants();
|
||||
|
@ -2171,35 +2181,36 @@ std::string GroupCall::chooseLargeVideoEndpoint() const {
|
|||
continue;
|
||||
}
|
||||
if (const auto participant = real->participantByEndpoint(endpoint)) {
|
||||
const auto peer = participant->peer;
|
||||
if (screenEndpoint.empty()
|
||||
&& participant->videoParams->screen.endpoint == endpoint) {
|
||||
screenEndpoint = endpoint;
|
||||
screenEndpoint = { peer, endpoint };
|
||||
break;
|
||||
}
|
||||
if (speakingEndpoint.empty() && participant->speaking) {
|
||||
speakingEndpoint = endpoint;
|
||||
speakingEndpoint = { peer, endpoint };
|
||||
}
|
||||
if (soundingEndpoint.empty() && participant->sounding) {
|
||||
soundingEndpoint = endpoint;
|
||||
soundingEndpoint = { peer, endpoint };
|
||||
}
|
||||
if (anyEndpoint.empty()) {
|
||||
anyEndpoint = endpoint;
|
||||
anyEndpoint = { peer, endpoint };
|
||||
}
|
||||
}
|
||||
}
|
||||
return !screenEndpoint.empty()
|
||||
return screenEndpoint
|
||||
? screenEndpoint
|
||||
: streamsVideo(myScreenEndpoint)
|
||||
? myScreenEndpoint
|
||||
: !speakingEndpoint.empty()
|
||||
? VideoEndpoint{ _joinAs, myScreenEndpoint }
|
||||
: speakingEndpoint
|
||||
? speakingEndpoint
|
||||
: !soundingEndpoint.empty()
|
||||
: soundingEndpoint
|
||||
? soundingEndpoint
|
||||
: !anyEndpoint.empty()
|
||||
: anyEndpoint
|
||||
? anyEndpoint
|
||||
: streamsVideo(myCameraEndpoint)
|
||||
? myCameraEndpoint
|
||||
: std::string();
|
||||
? VideoEndpoint{ _joinAs, myCameraEndpoint }
|
||||
: VideoEndpoint();
|
||||
}
|
||||
|
||||
void GroupCall::updateInstanceMuteState() {
|
||||
|
@ -2491,13 +2502,13 @@ void GroupCall::sendSelfUpdate(SendUpdateType type) {
|
|||
}).send();
|
||||
}
|
||||
|
||||
void GroupCall::pinVideoEndpoint(const std::string &endpoint) {
|
||||
if (endpoint.empty()) {
|
||||
_videoEndpointPinned = endpoint;
|
||||
} else if (streamsVideo(endpoint)) {
|
||||
_videoEndpointPinned = std::string();
|
||||
void GroupCall::pinVideoEndpoint(const VideoEndpoint &endpoint) {
|
||||
if (!endpoint) {
|
||||
_videoEndpointPinned = false;
|
||||
} else if (streamsVideo(endpoint.endpoint)) {
|
||||
_videoEndpointPinned = false;
|
||||
_videoEndpointLarge = endpoint;
|
||||
_videoEndpointPinned = endpoint;
|
||||
_videoEndpointPinned = true;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -74,6 +74,55 @@ struct LevelUpdate {
|
|||
bool me = false;
|
||||
};
|
||||
|
||||
struct VideoEndpoint {
|
||||
PeerData *peer = nullptr;
|
||||
std::string endpoint;
|
||||
|
||||
[[nodiscard]] bool empty() const noexcept {
|
||||
return !peer;
|
||||
}
|
||||
[[nodiscard]] explicit operator bool() const noexcept {
|
||||
return !empty();
|
||||
}
|
||||
};
|
||||
|
||||
inline bool operator==(
|
||||
const VideoEndpoint &a,
|
||||
const VideoEndpoint &b) noexcept {
|
||||
return (a.peer == b.peer) && (a.endpoint == b.endpoint);
|
||||
}
|
||||
|
||||
inline bool operator!=(
|
||||
const VideoEndpoint &a,
|
||||
const VideoEndpoint &b) noexcept {
|
||||
return !(a == b);
|
||||
}
|
||||
|
||||
inline bool operator<(
|
||||
const VideoEndpoint &a,
|
||||
const VideoEndpoint &b) noexcept {
|
||||
return (a.peer < b.peer)
|
||||
|| (a.peer == b.peer && a.endpoint < b.endpoint);
|
||||
}
|
||||
|
||||
inline bool operator>(
|
||||
const VideoEndpoint &a,
|
||||
const VideoEndpoint &b) noexcept {
|
||||
return (b < a);
|
||||
}
|
||||
|
||||
inline bool operator<=(
|
||||
const VideoEndpoint &a,
|
||||
const VideoEndpoint &b) noexcept {
|
||||
return !(b < a);
|
||||
}
|
||||
|
||||
inline bool operator>=(
|
||||
const VideoEndpoint &a,
|
||||
const VideoEndpoint &b) noexcept {
|
||||
return !(a < b);
|
||||
}
|
||||
|
||||
struct StreamsVideoUpdate {
|
||||
std::string endpoint;
|
||||
bool streams = false;
|
||||
|
@ -234,18 +283,18 @@ public:
|
|||
&& _incomingVideoEndpoints.contains(endpoint)
|
||||
&& activeVideoEndpointType(endpoint) != EndpointType::None;
|
||||
}
|
||||
[[nodiscard]] const std::string &videoEndpointPinned() const {
|
||||
[[nodiscard]] bool videoEndpointPinned() const {
|
||||
return _videoEndpointPinned.current();
|
||||
}
|
||||
[[nodiscard]] rpl::producer<std::string> videoEndpointPinnedValue() const {
|
||||
[[nodiscard]] rpl::producer<bool> videoEndpointPinnedValue() const {
|
||||
return _videoEndpointPinned.value();
|
||||
}
|
||||
void pinVideoEndpoint(const std::string &endpoint);
|
||||
[[nodiscard]] const std::string &videoEndpointLarge() const {
|
||||
void pinVideoEndpoint(const VideoEndpoint &endpoint);
|
||||
[[nodiscard]] const VideoEndpoint &videoEndpointLarge() const {
|
||||
return _videoEndpointLarge.current();
|
||||
}
|
||||
[[nodiscard]] auto videoEndpointLargeValue() const
|
||||
-> rpl::producer<std::string> {
|
||||
-> rpl::producer<VideoEndpoint> {
|
||||
return _videoEndpointLarge.value();
|
||||
}
|
||||
[[nodiscard]] Webrtc::VideoTrack *videoLargeTrack() const {
|
||||
|
@ -386,7 +435,7 @@ private:
|
|||
void setIncomingVideoEndpoints(
|
||||
const std::vector<std::string> &endpoints);
|
||||
void fillActiveVideoEndpoints();
|
||||
[[nodiscard]] std::string chooseLargeVideoEndpoint() const;
|
||||
[[nodiscard]] VideoEndpoint chooseLargeVideoEndpoint() const;
|
||||
[[nodiscard]] EndpointType activeVideoEndpointType(
|
||||
const std::string &endpoint) const;
|
||||
|
||||
|
@ -473,8 +522,8 @@ private:
|
|||
rpl::event_stream<StreamsVideoUpdate> _streamsVideoUpdated;
|
||||
base::flat_set<std::string> _incomingVideoEndpoints;
|
||||
base::flat_map<std::string, EndpointType> _activeVideoEndpoints;
|
||||
rpl::variable<std::string> _videoEndpointLarge;
|
||||
rpl::variable<std::string> _videoEndpointPinned;
|
||||
rpl::variable<VideoEndpoint> _videoEndpointLarge;
|
||||
rpl::variable<bool> _videoEndpointPinned;
|
||||
std::unique_ptr<LargeTrack> _videoLargeTrackWrap;
|
||||
rpl::variable<Webrtc::VideoTrack*> _videoLargeTrack;
|
||||
base::flat_map<uint32, Data::LastSpokeTimes> _lastSpoke;
|
||||
|
|
|
@ -1228,9 +1228,9 @@ void MembersController::setupListChangeViewers() {
|
|||
}, _lifetime);
|
||||
|
||||
_call->videoEndpointLargeValue(
|
||||
) | rpl::filter([=](const std::string &largeEndpoint) {
|
||||
return (_largeEndpoint != largeEndpoint);
|
||||
}) | rpl::start_with_next([=](const std::string &largeEndpoint) {
|
||||
) | rpl::filter([=](const VideoEndpoint &largeEndpoint) {
|
||||
return (_largeEndpoint != largeEndpoint.endpoint);
|
||||
}) | rpl::start_with_next([=](const VideoEndpoint &largeEndpoint) {
|
||||
if (_call->streamsVideo(_largeEndpoint)) {
|
||||
if (const auto participant = findParticipant(_largeEndpoint)) {
|
||||
if (const auto row = findRow(participant->peer)) {
|
||||
|
@ -1243,7 +1243,7 @@ void MembersController::setupListChangeViewers() {
|
|||
}
|
||||
}
|
||||
}
|
||||
_largeEndpoint = largeEndpoint;
|
||||
_largeEndpoint = largeEndpoint.endpoint;
|
||||
if (const auto participant = findParticipant(_largeEndpoint)) {
|
||||
if (const auto row = findRow(participant->peer)) {
|
||||
if (row->videoTrackEndpoint() == _largeEndpoint) {
|
||||
|
@ -2013,12 +2013,14 @@ base::unique_qptr<Ui::PopupMenu> MembersController::createRowContextMenu(
|
|||
});
|
||||
|
||||
if (const auto real = _call->lookupReal()) {
|
||||
const auto pinnedEndpoint = _call->videoEndpointPinned();
|
||||
const auto pinnedEndpoint = _call->videoEndpointPinned()
|
||||
? _call->videoEndpointLarge().endpoint
|
||||
: std::string();
|
||||
const auto participant = real->participantByEndpoint(pinnedEndpoint);
|
||||
if (participant && participant->peer == participantPeer) {
|
||||
result->addAction(
|
||||
tr::lng_group_call_context_unpin_camera(tr::now),
|
||||
[=] { _call->pinVideoEndpoint(std::string()); });
|
||||
[=] { _call->pinVideoEndpoint(VideoEndpoint()); });
|
||||
} else {
|
||||
const auto &participants = real->participants();
|
||||
const auto i = ranges::find(
|
||||
|
@ -2031,9 +2033,9 @@ base::unique_qptr<Ui::PopupMenu> MembersController::createRowContextMenu(
|
|||
const auto streamsScreen = _call->streamsVideo(screen);
|
||||
if (streamsScreen || _call->streamsVideo(camera)) {
|
||||
const auto callback = [=] {
|
||||
_call->pinVideoEndpoint(streamsScreen
|
||||
? screen
|
||||
: camera);
|
||||
_call->pinVideoEndpoint(VideoEndpoint{
|
||||
participantPeer,
|
||||
streamsScreen ? screen : camera });
|
||||
};
|
||||
result->addAction(
|
||||
tr::lng_group_call_context_pin_camera(tr::now),
|
||||
|
@ -2438,10 +2440,7 @@ void Members::setupPinnedVideo() {
|
|||
_mode.changes() | rpl::filter(
|
||||
_1 == PanelMode::Default
|
||||
) | rpl::to_empty,
|
||||
_call->videoEndpointLargeValue(
|
||||
) | rpl::filter([=](const std::string &endpoint) {
|
||||
return endpoint == _call->videoEndpointPinned();
|
||||
}) | rpl::to_empty
|
||||
_call->videoEndpointPinnedValue() | rpl::filter(_1) | rpl::to_empty
|
||||
) | rpl::start_with_next([=] {
|
||||
_scroll->scrollToY(0);
|
||||
}, _scroll->lifetime());
|
||||
|
|
Loading…
Add table
Reference in a new issue