/* 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/calls_volume_item.h" #include "calls/calls_group_common.h" #include "ui/effects/animation_value.h" #include "ui/effects/cross_line.h" #include "ui/widgets/continuous_sliders.h" #include "styles/style_calls.h" #include "ui/paint/arcs.h" namespace Calls { namespace { constexpr auto kMaxVolumePercent = 200; const auto kSpeakerThreshold = std::vector{ 10.0f / kMaxVolumePercent, 50.0f / kMaxVolumePercent, 150.0f / kMaxVolumePercent }; constexpr auto kVolumeStickedValues = std::array, 7>{{ { 25. / kMaxVolumePercent, 2. / kMaxVolumePercent }, { 50. / kMaxVolumePercent, 2. / kMaxVolumePercent }, { 75. / kMaxVolumePercent, 2. / kMaxVolumePercent }, { 100. / kMaxVolumePercent, 5. / kMaxVolumePercent }, { 125. / kMaxVolumePercent, 2. / kMaxVolumePercent }, { 150. / kMaxVolumePercent, 2. / kMaxVolumePercent }, { 175. / kMaxVolumePercent, 2. / kMaxVolumePercent }, }}; QString VolumeString(int volumePercent) { return u"%1%"_q.arg(volumePercent); } } // namespace MenuVolumeItem::MenuVolumeItem( not_null parent, const style::Menu &st, rpl::producer participantState, int startVolume, int maxVolume, bool muted) : Ui::Menu::ItemBase(parent, st) , _maxVolume(maxVolume) , _cloudMuted(muted) , _localMuted(muted) , _slider(base::make_unique_q( this, st::groupCallMenuVolumeSlider)) , _dummyAction(new QAction(parent)) , _st(st) , _stCross(st::groupCallMuteCrossLine) , _crossLineMute(std::make_unique(_stCross, true)) , _arcs(std::make_unique( st::groupCallSpeakerArcsAnimation, kSpeakerThreshold, _localMuted ? 0. : (startVolume / float(maxVolume)), Ui::Paint::ArcsAnimation::Direction::Right)) { initResizeHook(parent->sizeValue()); enableMouseSelecting(); enableMouseSelecting(_slider.get()); _slider->setAlwaysDisplayMarker(true); sizeValue( ) | rpl::start_with_next([=](const QSize &size) { const auto geometry = QRect(QPoint(), size); _itemRect = geometry - _st.itemPadding; _speakerRect = QRect(_itemRect.topLeft(), _stCross.icon.size()); _arcPosition = _speakerRect.center() + QPoint(0, st::groupCallMenuSpeakerArcsSkip); _volumeRect = QRect( _arcPosition.x() + st::groupCallMenuVolumeSkip + _arcs->finishedWidth(), _speakerRect.y(), _st.itemStyle.font->width(VolumeString(kMaxVolumePercent)), _speakerRect.height()); _slider->setGeometry(_itemRect - style::margins(0, contentHeight() / 2, 0, 0)); }, lifetime()); setCloudVolume(startVolume); paintRequest( ) | rpl::start_with_next([=](const QRect &clip) { Painter p(this); const auto volume = _localMuted ? 0 : std::round(_slider->value() * kMaxVolumePercent); const auto muteProgress = _crossLineAnimation.value((!volume) ? 1. : 0.); const auto selected = isSelected(); p.fillRect(clip, selected ? st.itemBgOver : st.itemBg); const auto mutePen = anim::color( unmuteColor(), muteColor(), muteProgress); p.setPen(mutePen); p.setFont(_st.itemStyle.font); p.drawText(_volumeRect, VolumeString(volume), style::al_left); _crossLineMute->paint( p, _speakerRect.topLeft(), muteProgress, (!muteProgress) ? std::nullopt : std::optional(mutePen)); { p.translate(_arcPosition); _arcs->paint(p); } }, lifetime()); _slider->setChangeProgressCallback([=](float64 value) { const auto newMuted = (value == 0); if (_localMuted != newMuted) { _localMuted = newMuted; _toggleMuteLocallyRequests.fire_copy(newMuted); _crossLineAnimation.start( [=] { update(_speakerRect.united(_volumeRect)); }, _localMuted ? 0. : 1., _localMuted ? 1. : 0., st::callPanelDuration); } if (value > 0) { _changeVolumeLocallyRequests.fire(value * _maxVolume); } update(_volumeRect); _arcs->setValue(value); }); const auto returnVolume = [=] { _changeVolumeLocallyRequests.fire_copy(_cloudVolume); }; _slider->setChangeFinishedCallback([=](float64 value) { const auto newVolume = std::round(value * _maxVolume); const auto muted = (value == 0); if (!_cloudMuted && muted) { returnVolume(); _localMuted = true; _toggleMuteRequests.fire(true); } if (_cloudMuted && muted) { returnVolume(); } if (_cloudMuted && !muted) { _waitingForUpdateVolume = true; _localMuted = false; _toggleMuteRequests.fire(false); } if (!_cloudMuted && !muted) { _changeVolumeRequests.fire_copy(newVolume); } }); std::move( participantState ) | rpl::start_with_next([=](const Group::ParticipantState &state) { const auto newMuted = state.mutedByMe; const auto newVolume = state.volume.value_or(0); _cloudMuted = _localMuted = newMuted; if (!newVolume) { return; } if (_waitingForUpdateVolume) { const auto localVolume = std::round(_slider->value() * _maxVolume); if ((localVolume != newVolume) && (_cloudVolume == newVolume)) { _changeVolumeRequests.fire(int(localVolume)); } } else { setCloudVolume(newVolume); } _waitingForUpdateVolume = false; }, lifetime()); _slider->setAdjustCallback([=](float64 value) { for (const auto &snap : kVolumeStickedValues) { if (value > (snap.first - snap.second) && value < (snap.first + snap.second)) { return snap.first; } } return value; }); initArcsAnimation(); } void MenuVolumeItem::initArcsAnimation() { const auto volumeLeftWas = lifetime().make_state(0); const auto lastTime = lifetime().make_state(0); _arcsAnimation.init([=](crl::time now) { _arcs->update(now); update(_speakerRect); const auto wasRect = _volumeRect; _volumeRect.moveLeft(anim::interpolate( *volumeLeftWas, _arcPosition.x() + st::groupCallMenuVolumeSkip + _arcs->finishedWidth(), std::clamp( (now - (*lastTime)) / float64(st::groupCallSpeakerArcsAnimation.duration), 0., 1.))); update(_speakerRect.united(wasRect.united(_volumeRect))); }); _arcs->startUpdateRequests( ) | rpl::start_with_next([=] { if (!_arcsAnimation.animating()) { *volumeLeftWas = _volumeRect.left(); *lastTime = crl::now(); _arcsAnimation.start(); } }, lifetime()); _arcs->stopUpdateRequests( ) | rpl::start_with_next([=] { _arcsAnimation.stop(); }, lifetime()); } QColor MenuVolumeItem::unmuteColor() const { return (isSelected() ? _st.itemFgOver : isEnabled() ? _st.itemFg : _st.itemFgDisabled)->c; } QColor MenuVolumeItem::muteColor() const { return (isSelected() ? st::attentionButtonFgOver : st::attentionButtonFg)->c; } void MenuVolumeItem::setCloudVolume(int volume) { if (_cloudVolume == volume) { return; } _cloudVolume = volume; if (!_slider->isChanging()) { setSliderVolume(_cloudMuted ? 0. : volume); } } void MenuVolumeItem::setSliderVolume(int volume) { _slider->setValue(float64(volume) / _maxVolume); update(_volumeRect); } not_null MenuVolumeItem::action() const { return _dummyAction; } bool MenuVolumeItem::isEnabled() const { return true; } int MenuVolumeItem::contentHeight() const { return _st.itemPadding.top() + _st.itemPadding.bottom() + _stCross.icon.height() * 2; } rpl::producer MenuVolumeItem::toggleMuteRequests() const { return _toggleMuteRequests.events(); } rpl::producer MenuVolumeItem::toggleMuteLocallyRequests() const { return _toggleMuteLocallyRequests.events(); } rpl::producer MenuVolumeItem::changeVolumeRequests() const { return _changeVolumeRequests.events(); } rpl::producer MenuVolumeItem::changeVolumeLocallyRequests() const { return _changeVolumeLocallyRequests.events(); } } // namespace Calls