From 69b9c63a6960840cd51829b4a81a391b4f2700f4 Mon Sep 17 00:00:00 2001 From: John Preston Date: Wed, 19 Jul 2023 23:59:28 +0400 Subject: [PATCH] Implement volume dropdown. --- .../stories/media_stories_controller.cpp | 16 ++ .../media/stories/media_stories_controller.h | 4 + .../media/stories/media_stories_delegate.h | 3 + .../media/stories/media_stories_header.cpp | 256 ++++++++++++++++-- .../media/stories/media_stories_header.h | 11 +- .../SourceFiles/media/view/media_view.style | 18 +- .../media/view/media_view_overlay_widget.cpp | 13 + .../media/view/media_view_overlay_widget.h | 3 + 8 files changed, 304 insertions(+), 20 deletions(-) diff --git a/Telegram/SourceFiles/media/stories/media_stories_controller.cpp b/Telegram/SourceFiles/media/stories/media_stories_controller.cpp index 62019349d..99ab50de7 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_controller.cpp +++ b/Telegram/SourceFiles/media/stories/media_stories_controller.cpp @@ -915,6 +915,22 @@ PauseState Controller::pauseState() const { : PauseState::Paused; } +float64 Controller::currentVolume() const { + return Core::App().settings().videoVolume(); +} + +void Controller::toggleVolume() { + _delegate->storiesVolumeToggle(); +} + +void Controller::changeVolume(float64 volume) { + _delegate->storiesVolumeChanged(volume); +} + +void Controller::volumeChangeFinished() { + _delegate->storiesVolumeChangeFinished(); +} + void Controller::updatePlayingAllowed() { if (!_shown) { return; diff --git a/Telegram/SourceFiles/media/stories/media_stories_controller.h b/Telegram/SourceFiles/media/stories/media_stories_controller.h index b29b6d520..8b804c598 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_controller.h +++ b/Telegram/SourceFiles/media/stories/media_stories_controller.h @@ -143,6 +143,10 @@ public: void setMenuShown(bool shown); [[nodiscard]] PauseState pauseState() const; + [[nodiscard]] float64 currentVolume() const; + void toggleVolume(); + void changeVolume(float64 volume); + void volumeChangeFinished(); void repaintSibling(not_null sibling); [[nodiscard]] SiblingView sibling(SiblingType type) const; diff --git a/Telegram/SourceFiles/media/stories/media_stories_delegate.h b/Telegram/SourceFiles/media/stories/media_stories_delegate.h index f42e3c463..ca663e69c 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_delegate.h +++ b/Telegram/SourceFiles/media/stories/media_stories_delegate.h @@ -61,6 +61,9 @@ public: [[nodiscard]] virtual float64 storiesSiblingOver(SiblingType type) = 0; virtual void storiesTogglePaused(bool paused) = 0; virtual void storiesRepaint() = 0; + virtual void storiesVolumeToggle() = 0; + virtual void storiesVolumeChanged(float64 volume) = 0; + virtual void storiesVolumeChangeFinished() = 0; }; } // namespace Media::Stories diff --git a/Telegram/SourceFiles/media/stories/media_stories_header.cpp b/Telegram/SourceFiles/media/stories/media_stories_header.cpp index cf752495a..04a0e3f25 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_header.cpp +++ b/Telegram/SourceFiles/media/stories/media_stories_header.cpp @@ -17,17 +17,24 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/text/format_values.h" #include "ui/text/text_utilities.h" #include "ui/widgets/buttons.h" +#include "ui/widgets/continuous_sliders.h" #include "ui/widgets/labels.h" #include "ui/wrap/fade_wrap.h" #include "ui/painter.h" #include "ui/rp_widget.h" #include "styles/style_media_view.h" +#include + namespace Media::Stories { namespace { constexpr auto kNameOpacity = 1.; constexpr auto kDateOpacity = 0.8; +constexpr auto kControlOpacity = 0.6; +constexpr auto kControlOpacityOver = 1.; +constexpr auto kVolumeHideTimeoutShort = crl::time(20); +constexpr auto kVolumeHideTimeoutLong = crl::time(200); struct Timestamp { QString text; @@ -267,6 +274,7 @@ void Header::show(HeaderData data) { || (data.fullCount && _data->fullIndex != data.fullIndex); _data = data; if (userChanged) { + _volume = nullptr; _date = nullptr; _name = nullptr; _userpic = nullptr; @@ -339,18 +347,8 @@ void Header::show(HeaderData data) { }); if (data.video) { - _playPause = std::make_unique( - _widget.get(), - st::storiesPlayButton); - _playPause->show(); - _playPause->setClickedCallback([=] { - _controller->togglePaused(_pauseState != PauseState::Paused); - }); - - _volumeToggle = std::make_unique( - _widget.get(), - st::storiesVolumeButton); - _volumeToggle->show(); + createPlayPause(); + createVolumeToggle(); _widget->widthValue() | rpl::start_with_next([=](int width) { const auto playPause = st::storiesPlayButtonPosition; @@ -362,6 +360,7 @@ void Header::show(HeaderData data) { _pauseState = _controller->pauseState(); applyPauseState(); } else { + _volume = nullptr; _playPause = nullptr; _volumeToggle = nullptr; } @@ -371,6 +370,223 @@ void Header::show(HeaderData data) { } } +void Header::createPlayPause() { + struct PlayPauseState { + Ui::Animations::Simple overAnimation; + bool over = false; + bool down = false; + }; + _playPause = std::make_unique(_widget.get()); + auto &lifetime = _playPause->lifetime(); + const auto state = lifetime.make_state(); + + _playPause->events( + ) | rpl::start_with_next([=](not_null e) { + const auto type = e->type(); + if (type == QEvent::Enter || type == QEvent::Leave) { + const auto over = (e->type() == QEvent::Enter); + if (state->over != over) { + state->over = over; + state->overAnimation.start( + [=] { _playPause->update(); }, + over ? 0. : 1., + over ? 1. : 0., + st::mediaviewFadeDuration); + } + } else if (type == QEvent::MouseButtonPress && state->over) { + state->down = true; + } else if (type == QEvent::MouseButtonRelease) { + const auto down = base::take(state->down); + if (down && state->over) { + _controller->togglePaused(_pauseState != PauseState::Paused); + } + } + }, lifetime); + + _playPause->paintRequest() | rpl::start_with_next([=] { + auto p = QPainter(_playPause.get()); + const auto paused = (_pauseState == PauseState::Paused); + const auto icon = paused + ? &st::storiesPlayIcon + : &st::storiesPauseIcon; + const auto over = state->overAnimation.value( + state->over ? 1. : 0.); + p.setOpacity(over * kControlOpacityOver + + (1. - over) * kControlOpacity); + icon->paint( + p, + st::storiesPlayButton.iconPosition, + _playPause->width()); + }, lifetime); + + _playPause->resize( + st::storiesPlayButton.width, + st::storiesPlayButton.height); + _playPause->show(); + _playPause->setCursor(style::cur_pointer); +} + +void Header::createVolumeToggle() { + struct VolumeState { + base::Timer hideTimer; + bool over = false; + bool dropdownOver = false; + }; + _volumeToggle = std::make_unique(_widget.get()); + auto &lifetime = _volumeToggle->lifetime(); + const auto state = lifetime.make_state(); + state->hideTimer.setCallback([=] { + _volume->toggle(false, anim::type::normal); + }); + + _volumeToggle->events( + ) | rpl::start_with_next([=](not_null e) { + const auto type = e->type(); + if (type == QEvent::Enter || type == QEvent::Leave) { + const auto over = (e->type() == QEvent::Enter); + if (state->over != over) { + state->over = over; + if (over) { + state->hideTimer.cancel(); + _volume->toggle(true, anim::type::normal); + } else if (!state->dropdownOver) { + state->hideTimer.callOnce(kVolumeHideTimeoutShort); + } + } + } + }, lifetime); + + _volumeToggle->paintRequest() | rpl::start_with_next([=] { + auto p = QPainter(_volumeToggle.get()); + p.setOpacity(kControlOpacity); + _volumeIcon.current()->paint( + p, + st::storiesVolumeButton.iconPosition, + _volumeToggle->width()); + }, lifetime); + updateVolumeIcon(); + + _volume = std::make_unique>( + _widget->parentWidget(), + object_ptr(_widget->parentWidget())); + _volume->events( + ) | rpl::start_with_next([=](not_null e) { + const auto type = e->type(); + if (type == QEvent::Enter || type == QEvent::Leave) { + const auto over = (e->type() == QEvent::Enter); + if (state->dropdownOver != over) { + state->dropdownOver = over; + if (over) { + state->hideTimer.cancel(); + _volume->toggle(true, anim::type::normal); + } else if (!state->over) { + state->hideTimer.callOnce(kVolumeHideTimeoutLong); + } + } + } + }, lifetime); + _controller->layoutValue( + ) | rpl::map([](const Layout &layout) { + return (layout.headerLayout == HeaderLayout::Outside); + }) | rpl::distinct_until_changed( + ) | rpl::start_with_next([=](bool horizontal) { + rebuildVolumeControls(_volume->entity(), horizontal); + }, lifetime); + + rpl::combine( + _widget->positionValue(), + _volumeToggle->positionValue(), + rpl::mappers::_1 + rpl::mappers::_2 + ) | rpl::start_with_next([=](QPoint position) { + _volume->move(position); + }, _volume->lifetime()); + + _volumeToggle->resize( + st::storiesVolumeButton.width, + st::storiesVolumeButton.height); + _volumeToggle->show(); + _volumeToggle->setCursor(style::cur_pointer); +} + +void Header::rebuildVolumeControls( + not_null dropdown, + bool horizontal) { + auto removed = false; + do { + removed = false; + for (const auto &child : dropdown->children()) { + if (child->isWidgetType()) { + removed = true; + delete child; + break; + } + } + } while (removed); + + const auto button = Ui::CreateChild( + dropdown.get(), + st::storiesVolumeButton); + _volumeIcon.value( + ) | rpl::start_with_next([=](const style::icon *icon) { + button->setIconOverride(icon, icon); + }, button->lifetime()); + + const auto slider = Ui::CreateChild( + dropdown.get(), + st::storiesVolumeSlider); + slider->setMoveByWheel(true); + slider->setAlwaysDisplayMarker(true); + using Direction = Ui::MediaSlider::Direction; + slider->setDirection(horizontal + ? Direction::Horizontal + : Direction::Vertical); + + slider->setChangeProgressCallback([=](float64 value) { + _controller->changeVolume(value); + updateVolumeIcon(); + }); + slider->setChangeFinishedCallback([=](float64 value) { + _controller->volumeChangeFinished(); + }); + button->setClickedCallback([=] { + _controller->toggleVolume(); + slider->setValue(_controller->currentVolume()); + updateVolumeIcon(); + }); + slider->setValue(_controller->currentVolume()); + + const auto skip = button->width() / 2; + const auto size = button->width() + + st::storiesVolumeSize + + st::storiesVolumeBottom; + const auto seekSize = st::storiesVolumeSlider.seekSize; + + button->move(0, 0); + if (horizontal) { + dropdown->resize(size, button->height()); + slider->resize(st::storiesVolumeSize, seekSize.height()); + slider->move( + button->width(), + (button->height() - slider->height()) / 2); + } else { + dropdown->resize(button->width(), size); + slider->resize(seekSize.width(), st::storiesVolumeSize); + slider->move( + (button->width() - slider->width()) / 2, + button->height()); + } + + dropdown->paintRequest( + ) | rpl::start_with_next([=] { + auto p = QPainter(dropdown); + auto hq = PainterHighQualityEnabler(p); + const auto radius = button->width() / 2.; + p.setPen(Qt::NoPen); + p.setBrush(st::mediaviewSaveMsgBg); + p.drawRoundedRect(dropdown->rect(), radius, radius); + }, button->lifetime()); +} + void Header::updatePauseState() { if (!_playPause) { return; @@ -380,17 +596,25 @@ void Header::updatePauseState() { } } +void Header::updateVolumeIcon() { + const auto volume = _controller->currentVolume(); + _volumeIcon = (volume <= 0.) + ? &st::mediaviewVolumeIcon0Over + : (volume < 1 / 2.) + ? &st::mediaviewVolumeIcon1Over + : &st::mediaviewVolumeIcon2Over; +} + void Header::applyPauseState() { Expects(_playPause != nullptr); - const auto paused = (_pauseState == PauseState::Paused); const auto inactive = (_pauseState == PauseState::Inactive); _playPause->setAttribute(Qt::WA_TransparentForMouseEvents, inactive); if (inactive) { - _playPause->clearState(); + QEvent e(QEvent::Leave); + QGuiApplication::sendEvent(_playPause.get(), &e); } - const auto icon = paused ? nullptr : &st::storiesPauseIcon; - _playPause->setIconOverride(icon, icon); + _playPause->update(); } void Header::raise() { diff --git a/Telegram/SourceFiles/media/stories/media_stories_header.h b/Telegram/SourceFiles/media/stories/media_stories_header.h index 8685e5bc8..b20f2b533 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_header.h +++ b/Telegram/SourceFiles/media/stories/media_stories_header.h @@ -49,6 +49,7 @@ public: ~Header(); void updatePauseState(); + void updateVolumeIcon(); void show(HeaderData data); void raise(); @@ -56,6 +57,11 @@ public: private: void updateDateText(); void applyPauseState(); + void createPlayPause(); + void createVolumeToggle(); + void rebuildVolumeControls( + not_null dropdown, + bool horizontal); const not_null _controller; @@ -66,9 +72,10 @@ private: std::unique_ptr _userpic; std::unique_ptr _name; std::unique_ptr _date; - std::unique_ptr _playPause; - std::unique_ptr _volumeToggle; + std::unique_ptr _playPause; + std::unique_ptr _volumeToggle; std::unique_ptr> _volume; + rpl::variable _volumeIcon; std::unique_ptr _privacy; std::optional _data; base::Timer _dateUpdateTimer; diff --git a/Telegram/SourceFiles/media/view/media_view.style b/Telegram/SourceFiles/media/view/media_view.style index a3ae2dac0..580347539 100644 --- a/Telegram/SourceFiles/media/view/media_view.style +++ b/Telegram/SourceFiles/media/view/media_view.style @@ -868,8 +868,22 @@ storiesPlayButton: IconButton(defaultIconButton) { } storiesPlayButtonPosition: point(54px, 0px); storiesVolumeButton: IconButton(storiesPlayButton) { - icon: mediaviewVolumeIcon2Over; - iconOver: mediaviewVolumeIcon2Over; + icon: mediaviewVolumeIcon0Over; + iconOver: mediaviewVolumeIcon0Over; ripple: emptyRippleAnimation; } storiesVolumeButtonPosition: point(10px, 0px); +storiesVolumeSize: 75px; +storiesVolumeBottom: 20px; +storiesVolumeSlider: MediaSlider { + width: 3px; + activeFg: mediaviewPlaybackActiveOver; + inactiveFg: mediaviewPlaybackActive; + activeFgOver: mediaviewPlaybackActiveOver; + inactiveFgOver: mediaviewPlaybackActive; + activeFgDisabled: mediaviewPlaybackActiveOver; + inactiveFgDisabled: mediaviewPlaybackActive; + receivedTillFg: mediaviewPlaybackActive; + seekSize: size(12px, 12px); + duration: mediaviewOverDuration; +} diff --git a/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp b/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp index 379c341c0..dfa0f9085 100644 --- a/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp +++ b/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp @@ -4219,10 +4219,23 @@ float64 OverlayWidget::storiesSiblingOver(Stories::SiblingType type) { ? overLevel(Over::LeftStories) : overLevel(Over::RightStories); } + void OverlayWidget::storiesRepaint() { update(); } +void OverlayWidget::storiesVolumeToggle() { + playbackControlsVolumeToggled(); +} + +void OverlayWidget::storiesVolumeChanged(float64 volume) { + playbackControlsVolumeChanged(volume); +} + +void OverlayWidget::storiesVolumeChangeFinished() { + playbackControlsVolumeChangeFinished(); +} + void OverlayWidget::playbackToggleFullScreen() { Expects(_streamed != nullptr); diff --git a/Telegram/SourceFiles/media/view/media_view_overlay_widget.h b/Telegram/SourceFiles/media/view/media_view_overlay_widget.h index b5be1b16c..cbb9165d9 100644 --- a/Telegram/SourceFiles/media/view/media_view_overlay_widget.h +++ b/Telegram/SourceFiles/media/view/media_view_overlay_widget.h @@ -261,6 +261,9 @@ private: void storiesTogglePaused(bool paused) override; float64 storiesSiblingOver(Stories::SiblingType type) override; void storiesRepaint() override; + void storiesVolumeToggle() override; + void storiesVolumeChanged(float64 volume) override; + void storiesVolumeChangeFinished() override; void hideControls(bool force = false); void subscribeToScreenGeometry();