// This file is part of Desktop App Toolkit, // a set of libraries for developing nice desktop applications. // // For license and copyright information please follow this link: // https://github.com/desktop-app/legal/blob/master/LEGAL // #include "ui/widgets/call_mute_button.h" #include "base/event_filter.h" #include "base/flat_map.h" #include "ui/effects/gradient.h" #include "ui/effects/radial_animation.h" #include "ui/paint/blobs.h" #include "ui/painter.h" #include "styles/palette.h" #include "styles/style_widgets.h" namespace Ui { namespace { using Radiuses = Paint::BlobBezier::Radiuses; constexpr auto kMaxLevel = 1.; constexpr auto kLevelDuration = 100. + 500. * 0.33; constexpr auto kScaleBig = 0.807 - 0.1; constexpr auto kScaleSmall = 0.704 - 0.1; constexpr auto kScaleBigMin = 0.878; constexpr auto kScaleSmallMin = 0.926; constexpr auto kScaleBigMax = (float)(kScaleBigMin + kScaleBig); constexpr auto kScaleSmallMax = (float)(kScaleSmallMin + kScaleSmall); constexpr auto kMainRadiusFactor = (float)(50. / 57.); constexpr auto kGlowPaddingFactor = 1.2; constexpr auto kGlowMinScale = 0.6; constexpr auto kGlowAlpha = 150; constexpr auto kOverrideColorBgAlpha = 76; constexpr auto kOverrideColorRippleAlpha = 50; constexpr auto kSwitchStateDuration = 120; auto MuteBlobs() -> std::array { return {{ { .segmentsCount = 6, .minScale = 1., .minRadius = st::callMuteMainBlobMinRadius * kMainRadiusFactor, .maxRadius = st::callMuteMainBlobMaxRadius * kMainRadiusFactor, .speedScale = .4, .alpha = 1., }, { .segmentsCount = 9, .minScale = kScaleSmallMin / kScaleSmallMax, .minRadius = st::callMuteMinorBlobMinRadius * kScaleSmallMax * kMainRadiusFactor, .maxRadius = st::callMuteMinorBlobMaxRadius * kScaleSmallMax * kMainRadiusFactor, .speedScale = 1., .alpha = (76. / 255.), }, { .segmentsCount = 12, .minScale = kScaleBigMin / kScaleBigMax, .minRadius = st::callMuteMajorBlobMinRadius * kScaleBigMax * kMainRadiusFactor, .maxRadius = st::callMuteMajorBlobMaxRadius * kScaleBigMax * kMainRadiusFactor, .speedScale = 1., .alpha = (76. / 255.), }, }}; } auto Colors() { using Vector = std::vector; return base::flat_map{ { CallMuteButtonType::ForceMuted, Vector{ st::callIconBg->c, st::callIconBg->c } }, { CallMuteButtonType::Active, Vector{ st::groupCallLive1->c, st::groupCallLive2->c } }, { CallMuteButtonType::Connecting, Vector{ st::callIconBg->c, st::callIconBg->c } }, { CallMuteButtonType::Muted, Vector{ st::groupCallMuted1->c, st::groupCallMuted2->c } }, }; } inline float64 InterpolateF(int a, int b, float64 b_ratio) { return a + float64(b - a) * b_ratio; } bool IsMuted(CallMuteButtonType type) { return (type != CallMuteButtonType::Active); } bool IsConnecting(CallMuteButtonType type) { return (type == CallMuteButtonType::Connecting); } bool IsInactive(CallMuteButtonType type) { return IsConnecting(type) || (type == CallMuteButtonType::ForceMuted); } } // namespace class BlobsWidget final : public RpWidget { public: BlobsWidget( not_null parent, rpl::producer &&hideBlobs); void setLevel(float level); void setBlobBrush(QBrush brush); void setGlowBrush(QBrush brush); [[nodiscard]] QRect innerRect() const; private: void init(); Paint::Blobs _blobs; QBrush _blobBrush; QBrush _glowBrush; int _center = 0; QRect _inner; crl::time _blobsLastTime = 0; Animations::Basic _animation; }; BlobsWidget::BlobsWidget( not_null parent, rpl::producer &&hideBlobs) : RpWidget(parent) , _blobs(MuteBlobs() | ranges::to_vector, kLevelDuration, kMaxLevel) , _blobBrush(Qt::transparent) , _glowBrush(Qt::transparent) , _blobsLastTime(crl::now()) { init(); for (auto i = 0; i < _blobs.size(); i++) { const auto radiuses = _blobs.radiusesAt(i); auto radiusesChange = rpl::duplicate( hideBlobs ) | rpl::map([=](bool hide) -> Radiuses { return hide ? Radiuses{ radiuses.min, radiuses.min } : radiuses; }) | rpl::distinct_until_changed(); _blobs.setRadiusesAt(std::move(radiusesChange), i); } } void BlobsWidget::init() { setAttribute(Qt::WA_TransparentForMouseEvents); { const auto s = _blobs.maxRadius() * 2 * kGlowPaddingFactor; resize(s, s); } sizeValue( ) | rpl::start_with_next([=](QSize size) { _center = size.width() / 2; const auto w = _blobs.maxRadius() * 2; const auto margins = style::margins(w, w, w, w); _inner = QRect(QPoint(), size).marginsRemoved(margins); }, lifetime()); paintRequest( ) | rpl::start_with_next([=] { Painter p(this); PainterHighQualityEnabler hq(p); if (anim::Disabled()) { p.translate(_center, _center); p.setPen(Qt::NoPen); p.setBrush(_blobBrush); const auto radius = st::callMuteMainBlobMinRadius * kMainRadiusFactor; p.drawEllipse(QPointF(), radius, radius); return; } // Glow. const auto s = kGlowMinScale + (1. - kGlowMinScale) * _blobs.currentLevel(); p.translate(_center, _center); p.scale(s, s); p.translate(-_center, -_center); p.fillRect(rect(), _glowBrush); p.resetTransform(); // Blobs. p.translate(_center, _center); _blobs.paint(p, _blobBrush); }, lifetime()); _animation.init([=](crl::time now) { _blobs.updateLevel(now - _blobsLastTime); _blobsLastTime = now; update(); return true; }); shownValue( ) | rpl::start_with_next([=](bool shown) { if (shown) { _animation.start(); } else { _animation.stop(); } }, lifetime()); } QRect BlobsWidget::innerRect() const { return _inner; } void BlobsWidget::setBlobBrush(QBrush brush) { if (_blobBrush == brush) { return; } _blobBrush = brush; } void BlobsWidget::setGlowBrush(QBrush brush) { if (_glowBrush == brush) { return; } _glowBrush = brush; } void BlobsWidget::setLevel(float level) { _blobs.setLevel(level); } CallMuteButton::CallMuteButton( not_null parent, CallMuteButtonState initial) : _state(initial) , _blobs(base::make_unique_q( parent, _state.value( ) | rpl::map([](const CallMuteButtonState &state) { return IsConnecting(state.type); }))) , _content(parent, st::callMuteButtonActive, &st::callMuteButtonMuted) , _radial(nullptr) , _colors(Colors()) , _crossLineMuteAnimation(st::callMuteCrossLine) { init(); style::PaletteChanged( ) | rpl::start_with_next([=] { _crossLineMuteAnimation.invalidate(); }, lifetime()); } void CallMuteButton::init() { // Label text. auto text = _state.value( ) | rpl::map([](const CallMuteButtonState &state) { return state.text; }); _content.setText(std::move(text)); _radialShowProgress.value( ) | rpl::start_with_next([=](float64 value) { if (((value == 0.) || anim::Disabled()) && _radial) { _radial->stop(); _radial = nullptr; return; } if ((value > 0.) && !anim::Disabled() && !_radial) { _radial = std::make_unique( [=] { _content.update(); }, st::callConnectingRadial); _radial->start(); } }, lifetime()); // State type. const auto previousType = lifetime().make_state(_state.current().type); setEnableMouse(false); const auto blobsInner = _blobs->innerRect(); auto linearGradients = anim::linear_gradients( _colors, QPointF(blobsInner.x(), blobsInner.y() + blobsInner.height()), QPointF(blobsInner.x() + blobsInner.width(), blobsInner.y())); auto glowColors = [&] { auto copy = _colors; for (auto &[type, colors] : copy) { if (IsInactive(type)) { colors[0] = st::groupCallBg->c; } else { colors[0].setAlpha(kGlowAlpha); } colors[1] = QColor(Qt::transparent); } return copy; }(); auto glows = anim::radial_gradients( std::move(glowColors), blobsInner.center(), _blobs->width() / 2); _state.value( ) | rpl::map([](const CallMuteButtonState &state) { return state.type; }) | rpl::start_with_next([=](CallMuteButtonType type) { const auto previous = *previousType; *previousType = type; if (IsInactive(type) && !IsInactive(previous)) { setEnableMouse(false); } const auto crossFrom = IsMuted(previous) ? 0. : 1.; const auto crossTo = IsMuted(type) ? 0. : 1.; const auto radialShowFrom = IsConnecting(previous) ? 1. : 0.; const auto radialShowTo = IsConnecting(type) ? 1. : 0.; const auto from = _switchAnimation.animating() ? (1. - _switchAnimation.value(0.)) : 0.; const auto to = 1.; auto callback = [=](float64 value) { _blobs->setBlobBrush(QBrush( linearGradients.gradient(previous, type, value))); _blobs->setGlowBrush(QBrush( glows.gradient(previous, type, value))); _blobs->update(); const auto crossProgress = (crossFrom == crossTo) ? crossTo : InterpolateF(crossFrom, crossTo, value); if (crossProgress != _crossLineProgress) { _crossLineProgress = crossProgress; _content.update(_muteIconPosition); } const auto radialShowProgress = (radialShowFrom == radialShowTo) ? radialShowTo : InterpolateF(radialShowFrom, radialShowTo, value); if (radialShowProgress != _radialShowProgress.current()) { _radialShowProgress = radialShowProgress; } overridesColors(previous, type, value); if (value == to) { if (!IsInactive(type) && IsInactive(previous)) { setEnableMouse(true); } } }; _switchAnimation.stop(); const auto duration = (1. - from) * kSwitchStateDuration; _switchAnimation.start(std::move(callback), from, to, duration); }, lifetime()); // Icon rect. _content.sizeValue( ) | rpl::start_with_next([=](QSize size) { const auto &icon = st::callMuteButtonActive.button.icon; const auto &pos = st::callMuteButtonActive.button.iconPosition; _muteIconPosition = QRect( (pos.x() < 0) ? ((size.width() - icon.width()) / 2) : pos.x(), (pos.y() < 0) ? ((size.height() - icon.height()) / 2) : pos.y(), icon.width(), icon.height()); }, lifetime()); // Paint. auto filterCallback = [=](not_null e) { if (e->type() != QEvent::Paint) { return base::EventFilterResult::Continue; } contentPaint(); return base::EventFilterResult::Cancel; }; auto filter = base::install_event_filter( &_content, std::move(filterCallback)); lifetime().make_state>(std::move(filter)); } void CallMuteButton::contentPaint() { Painter p(&_content); const auto progress = 1. - _crossLineProgress; _crossLineMuteAnimation.paint(p, _muteIconPosition.topLeft(), progress); if (_radial) { p.setOpacity(_radialShowProgress.current()); _radial->draw( p, st::callMuteButtonActive.bgPosition, _content.width()); } } void CallMuteButton::setState(const CallMuteButtonState &state) { _state = state; } void CallMuteButton::setLevel(float level) { _level = level; _blobs->setLevel(level); } rpl::producer CallMuteButton::clicks() const { return _content.clicks(); } QSize CallMuteButton::innerSize() const { return innerGeometry().size(); } QRect CallMuteButton::innerGeometry() const { const auto skip = st::callMuteButtonActive.outerRadius; return QRect( _content.x(), _content.y(), _content.width() - 2 * skip, _content.width() - 2 * skip); } void CallMuteButton::moveInner(QPoint position) { const auto skip = st::callMuteButtonActive.outerRadius; _content.move(position - QPoint(skip, skip)); { const auto offset = QPoint( (_blobs->width() - _content.width()) / 2, (_blobs->height() - _content.width()) / 2); _blobs->move(_content.pos() - offset); } } void CallMuteButton::setVisible(bool visible) { _content.setVisible(visible); _blobs->setVisible(visible); } void CallMuteButton::raise() { _blobs->raise(); _content.raise(); } void CallMuteButton::lower() { _content.lower(); _blobs->lower(); } void CallMuteButton::setEnableMouse(bool value) { _content.setAttribute(Qt::WA_TransparentForMouseEvents, !value); } void CallMuteButton::overridesColors( CallMuteButtonType fromType, CallMuteButtonType toType, float64 progress) { const auto toInactive = IsInactive(toType); const auto fromInactive = IsInactive(fromType); if (toInactive && (progress == 1)) { _colorOverrides.fire({ std::nullopt, std::nullopt }); return; } auto from = _colors.find(fromType)->second[0]; auto to = _colors.find(toType)->second[0]; auto fromRipple = from; auto toRipple = to; if (!toInactive) { toRipple.setAlpha(kOverrideColorRippleAlpha); to.setAlpha(kOverrideColorBgAlpha); } if (!fromInactive) { fromRipple.setAlpha(kOverrideColorRippleAlpha); from.setAlpha(kOverrideColorBgAlpha); } const auto resultBg = anim::color(from, to, progress); const auto resultRipple = anim::color(fromRipple, toRipple, progress); _colorOverrides.fire({ resultBg, resultRipple }); } rpl::producer CallMuteButton::colorOverrides() const { return _colorOverrides.events(); } rpl::lifetime &CallMuteButton::lifetime() { return _blobs->lifetime(); } CallMuteButton::~CallMuteButton() = default; } // namespace Ui