1179 lines
31 KiB
C++
1179 lines
31 KiB
C++
// 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/controls/call_mute_button.h"
|
|
|
|
#include "base/flat_map.h"
|
|
#include "ui/abstract_button.h"
|
|
#include "ui/paint/blobs.h"
|
|
#include "ui/painter.h"
|
|
#include "ui/widgets/call_button.h"
|
|
#include "ui/widgets/labels.h"
|
|
#include "base/openssl_help.h"
|
|
#include "styles/palette.h"
|
|
#include "styles/style_widgets.h"
|
|
#include "styles/style_calls.h"
|
|
|
|
#include <QtCore/QtMath>
|
|
#include <QtCore/QCoreApplication>
|
|
|
|
namespace Ui {
|
|
namespace {
|
|
|
|
using Radiuses = Paint::Blob::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 kShiftDuration = crl::time(300);
|
|
constexpr auto kSwitchStateDuration = crl::time(120);
|
|
constexpr auto kSwitchLabelDuration = crl::time(180);
|
|
|
|
// Switch state from Connecting animation.
|
|
constexpr auto kSwitchRadialDuration = crl::time(350);
|
|
constexpr auto kSwitchCirclelDuration = crl::time(275);
|
|
constexpr auto kBlobsScaleEnterDuration = crl::time(400);
|
|
constexpr auto kSwitchStateFromConnectingDuration = kSwitchRadialDuration
|
|
+ kSwitchCirclelDuration
|
|
+ kBlobsScaleEnterDuration;
|
|
|
|
constexpr auto kRadialEndPartAnimation = float(kSwitchRadialDuration)
|
|
/ kSwitchStateFromConnectingDuration;
|
|
constexpr auto kBlobsWidgetPartAnimation = 1. - kRadialEndPartAnimation;
|
|
constexpr auto kFillCirclePartAnimation = float(kSwitchCirclelDuration)
|
|
/ (kSwitchCirclelDuration + kBlobsScaleEnterDuration);
|
|
constexpr auto kBlobPartAnimation = float(kBlobsScaleEnterDuration)
|
|
/ (kSwitchCirclelDuration + kBlobsScaleEnterDuration);
|
|
|
|
constexpr auto kOverlapProgressRadialHide = 1.2;
|
|
|
|
constexpr auto kRadialFinishArcShift = 1200;
|
|
|
|
[[nodiscard]] CallMuteButtonType TypeForIcon(CallMuteButtonType type) {
|
|
return (type == CallMuteButtonType::Connecting)
|
|
? CallMuteButtonType::Muted
|
|
: (type == CallMuteButtonType::RaisedHand)
|
|
? CallMuteButtonType::ForceMuted
|
|
: type;
|
|
};
|
|
|
|
auto MuteBlobs() {
|
|
return std::vector<Paint::Blobs::BlobData>{
|
|
{
|
|
.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<QColor>;
|
|
using Colors = anim::gradient_colors;
|
|
auto result = base::flat_map<CallMuteButtonType, Colors>{
|
|
{
|
|
CallMuteButtonType::Active,
|
|
Colors(Vector{ st::groupCallLive1->c, st::groupCallLive2->c })
|
|
},
|
|
{
|
|
CallMuteButtonType::Connecting,
|
|
Colors(st::callIconBg->c)
|
|
},
|
|
{
|
|
CallMuteButtonType::Muted,
|
|
Colors(Vector{ st::groupCallMuted1->c, st::groupCallMuted2->c })
|
|
},
|
|
};
|
|
const auto forceMutedColors = Colors(QGradientStops{
|
|
{ .0, st::groupCallForceMuted3->c },
|
|
{ .5, st::groupCallForceMuted2->c },
|
|
{ 1., st::groupCallForceMuted1->c } });
|
|
const auto forceMutedTypes = {
|
|
CallMuteButtonType::ForceMuted,
|
|
CallMuteButtonType::RaisedHand,
|
|
CallMuteButtonType::ScheduledCanStart,
|
|
CallMuteButtonType::ScheduledNotify,
|
|
CallMuteButtonType::ScheduledSilent,
|
|
};
|
|
for (const auto type : forceMutedTypes) {
|
|
result.emplace(type, forceMutedColors);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
bool IsMuted(CallMuteButtonType type) {
|
|
return (type != CallMuteButtonType::Active);
|
|
}
|
|
|
|
bool IsConnecting(CallMuteButtonType type) {
|
|
return (type == CallMuteButtonType::Connecting);
|
|
}
|
|
|
|
bool IsInactive(CallMuteButtonType type) {
|
|
return IsConnecting(type);
|
|
}
|
|
|
|
auto Clamp(float64 value) {
|
|
return std::clamp(value, 0., 1.);
|
|
}
|
|
|
|
void ComputeRadialFinish(
|
|
int &value,
|
|
float64 progress,
|
|
int to = -RadialState::kFull) {
|
|
value = anim::interpolate(value, to, Clamp(progress));
|
|
}
|
|
|
|
} // namespace
|
|
|
|
class AnimatedLabel final : public RpWidget {
|
|
public:
|
|
AnimatedLabel(
|
|
QWidget *parent,
|
|
rpl::producer<QString> &&text,
|
|
crl::time duration,
|
|
int additionalHeight,
|
|
const style::FlatLabel &st = st::defaultFlatLabel);
|
|
|
|
int contentHeight() const;
|
|
|
|
private:
|
|
void setText(const QString &text);
|
|
|
|
const style::FlatLabel &_st;
|
|
const crl::time _duration;
|
|
const int _additionalHeight;
|
|
const TextParseOptions _options;
|
|
|
|
Text::String _text;
|
|
Text::String _previousText;
|
|
|
|
Animations::Simple _animation;
|
|
|
|
};
|
|
|
|
AnimatedLabel::AnimatedLabel(
|
|
QWidget *parent,
|
|
rpl::producer<QString> &&text,
|
|
crl::time duration,
|
|
int additionalHeight,
|
|
const style::FlatLabel &st)
|
|
: RpWidget(parent)
|
|
, _st(st)
|
|
, _duration(duration)
|
|
, _additionalHeight(additionalHeight)
|
|
, _options({ 0, 0, 0, Qt::LayoutDirectionAuto }) {
|
|
std::move(
|
|
text
|
|
) | rpl::start_with_next([=](const QString &value) {
|
|
setText(value);
|
|
}, lifetime());
|
|
|
|
paintRequest(
|
|
) | rpl::start_with_next([=] {
|
|
Painter p(this);
|
|
const auto progress = _animation.value(1.);
|
|
|
|
p.setFont(_st.style.font);
|
|
p.setPen(_st.textFg);
|
|
p.setTextPalette(_st.palette);
|
|
|
|
const auto textHeight = contentHeight();
|
|
const auto diffHeight = height() - textHeight;
|
|
const auto center = diffHeight / 2;
|
|
|
|
p.setOpacity(1. - progress);
|
|
if (p.opacity()) {
|
|
_previousText.draw(
|
|
p,
|
|
0,
|
|
anim::interpolate(center, diffHeight, progress),
|
|
width(),
|
|
style::al_center);
|
|
}
|
|
|
|
p.setOpacity(progress);
|
|
if (p.opacity()) {
|
|
_text.draw(
|
|
p,
|
|
0,
|
|
anim::interpolate(0, center, progress),
|
|
width(),
|
|
style::al_center);
|
|
}
|
|
}, lifetime());
|
|
}
|
|
|
|
int AnimatedLabel::contentHeight() const {
|
|
return _st.style.font->height;
|
|
}
|
|
|
|
void AnimatedLabel::setText(const QString &text) {
|
|
if (_text.toString() == text) {
|
|
return;
|
|
}
|
|
_previousText = _text;
|
|
_text.setText(_st.style, text, _options);
|
|
|
|
const auto width = std::max(
|
|
_st.style.font->width(_text.toString()),
|
|
_st.style.font->width(_previousText.toString()));
|
|
resize(
|
|
width + _additionalHeight,
|
|
contentHeight() + _additionalHeight * 2);
|
|
|
|
_animation.stop();
|
|
_animation.start([=] { update(); }, 0., 1., _duration);
|
|
}
|
|
|
|
class BlobsWidget final : public RpWidget {
|
|
public:
|
|
BlobsWidget(
|
|
not_null<RpWidget*> parent,
|
|
int diameter,
|
|
rpl::producer<bool> &&hideBlobs);
|
|
|
|
void setDiameter(int diameter);
|
|
void setLevel(float level);
|
|
void setBlobBrush(QBrush brush);
|
|
void setGlowBrush(QBrush brush);
|
|
|
|
[[nodiscard]] QRectF innerRect() const;
|
|
|
|
[[nodiscard]] float64 switchConnectingProgress() const;
|
|
void setSwitchConnectingProgress(float64 progress);
|
|
|
|
private:
|
|
void init(int diameter);
|
|
void computeCircleRect();
|
|
|
|
Paint::Blobs _blobs;
|
|
|
|
float _circleRadius = 0.;
|
|
QBrush _blobBrush;
|
|
QBrush _glowBrush;
|
|
int _center = 0;
|
|
QRectF _circleRect;
|
|
|
|
float64 _switchConnectingProgress = 0.;
|
|
|
|
crl::time _blobsLastTime = 0;
|
|
crl::time _blobsHideLastTime = 0;
|
|
|
|
float64 _blobsScaleEnter = 0.;
|
|
crl::time _blobsScaleLastTime = 0;
|
|
|
|
bool _hideBlobs = true;
|
|
|
|
Animations::Basic _animation;
|
|
|
|
};
|
|
|
|
BlobsWidget::BlobsWidget(
|
|
not_null<RpWidget*> parent,
|
|
int diameter,
|
|
rpl::producer<bool> &&hideBlobs)
|
|
: RpWidget(parent)
|
|
, _blobs(MuteBlobs(), kLevelDuration, kMaxLevel)
|
|
, _blobBrush(Qt::transparent)
|
|
, _glowBrush(Qt::transparent)
|
|
, _blobsLastTime(crl::now())
|
|
, _blobsScaleLastTime(crl::now()) {
|
|
init(diameter);
|
|
|
|
std::move(
|
|
hideBlobs
|
|
) | rpl::start_with_next([=](bool hide) {
|
|
if (_hideBlobs != hide) {
|
|
const auto now = crl::now();
|
|
if ((now - _blobsScaleLastTime) >= kBlobsScaleEnterDuration) {
|
|
_blobsScaleLastTime = now;
|
|
}
|
|
_hideBlobs = hide;
|
|
}
|
|
if (hide) {
|
|
setLevel(0.);
|
|
}
|
|
_blobsHideLastTime = hide ? crl::now() : 0;
|
|
if (!hide && !_animation.animating()) {
|
|
_animation.start();
|
|
}
|
|
}, lifetime());
|
|
}
|
|
|
|
void BlobsWidget::setDiameter(int diameter) {
|
|
_circleRadius = diameter / 2.;
|
|
const auto defaultSize = _blobs.maxRadius() * 2 * kGlowPaddingFactor;
|
|
const auto s = int(std::ceil((defaultSize * diameter)
|
|
/ float64(st::callMuteBlobRadiusForDiameter)));
|
|
const auto size = QSize{ s, s };
|
|
if (this->size() != size) {
|
|
resize(size);
|
|
}
|
|
computeCircleRect();
|
|
}
|
|
|
|
void BlobsWidget::computeCircleRect() {
|
|
const auto &r = _circleRadius;
|
|
const auto left = (size().width() - r * 2.) / 2.;
|
|
const auto add = st::callConnectingRadial.thickness / 2;
|
|
_circleRect = QRectF(left, left, r * 2, r * 2).marginsAdded(
|
|
style::margins(add, add, add, add));
|
|
}
|
|
|
|
void BlobsWidget::init(int diameter) {
|
|
setAttribute(Qt::WA_TransparentForMouseEvents);
|
|
|
|
const auto cutRect = [](Painter &p, const QRectF &r) {
|
|
p.save();
|
|
p.setOpacity(1.);
|
|
p.setBrush(st::groupCallBg);
|
|
p.setCompositionMode(QPainter::CompositionMode_Source);
|
|
p.drawEllipse(r);
|
|
p.restore();
|
|
};
|
|
|
|
setDiameter(diameter);
|
|
|
|
sizeValue(
|
|
) | rpl::start_with_next([=](QSize size) {
|
|
_center = size.width() / 2;
|
|
computeCircleRect();
|
|
}, lifetime());
|
|
|
|
paintRequest(
|
|
) | rpl::start_with_next([=] {
|
|
Painter p(this);
|
|
PainterHighQualityEnabler hq(p);
|
|
|
|
p.setPen(Qt::NoPen);
|
|
|
|
// 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);
|
|
const auto scale = (_switchConnectingProgress > 0.)
|
|
? anim::easeOutBack(
|
|
1.,
|
|
_blobsScaleEnter * (1. - Clamp(
|
|
_switchConnectingProgress / kBlobPartAnimation)))
|
|
: _blobsScaleEnter;
|
|
const auto sizeScale = (2. * _circleRadius)
|
|
/ st::callMuteBlobRadiusForDiameter;
|
|
_blobs.paint(p, _blobBrush, scale * sizeScale);
|
|
p.translate(-_center, -_center);
|
|
|
|
if (scale < 1.) {
|
|
cutRect(p, _circleRect);
|
|
}
|
|
|
|
// Main circle.
|
|
const auto circleProgress =
|
|
Clamp(_switchConnectingProgress - kBlobPartAnimation)
|
|
/ kFillCirclePartAnimation;
|
|
const auto skipColoredCircle = (circleProgress == 1.);
|
|
|
|
if (!skipColoredCircle) {
|
|
p.setBrush(_blobBrush);
|
|
p.drawEllipse(_circleRect);
|
|
}
|
|
|
|
if (_switchConnectingProgress > 0.) {
|
|
p.resetTransform();
|
|
|
|
const auto mF = (_circleRect.width() / 2) * (1. - circleProgress);
|
|
const auto cutOutRect = _circleRect.marginsRemoved(
|
|
QMarginsF(mF, mF, mF, mF));
|
|
|
|
if (!skipColoredCircle) {
|
|
p.setBrush(st::callConnectingRadial.color);
|
|
p.setOpacity(circleProgress);
|
|
p.drawEllipse(_circleRect);
|
|
}
|
|
|
|
p.setOpacity(1.);
|
|
|
|
cutRect(p, cutOutRect);
|
|
|
|
p.setBrush(st::callIconBg);
|
|
p.drawEllipse(cutOutRect);
|
|
}
|
|
}, lifetime());
|
|
|
|
_animation.init([=](crl::time now) {
|
|
if (const auto &last = _blobsHideLastTime; (last > 0)
|
|
&& (now - last >= kBlobsScaleEnterDuration)) {
|
|
_animation.stop();
|
|
return false;
|
|
}
|
|
_blobs.updateLevel(now - _blobsLastTime);
|
|
_blobsLastTime = now;
|
|
|
|
const auto dt = Clamp(
|
|
(now - _blobsScaleLastTime) / float64(kBlobsScaleEnterDuration));
|
|
_blobsScaleEnter = _hideBlobs
|
|
? (1. - anim::easeInCirc(1., dt))
|
|
: anim::easeOutBack(1., dt);
|
|
|
|
update();
|
|
return true;
|
|
});
|
|
shownValue(
|
|
) | rpl::start_with_next([=](bool shown) {
|
|
if (shown) {
|
|
_animation.start();
|
|
} else {
|
|
_animation.stop();
|
|
}
|
|
}, lifetime());
|
|
}
|
|
|
|
QRectF BlobsWidget::innerRect() const {
|
|
return _circleRect;
|
|
}
|
|
|
|
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) {
|
|
if (_blobsHideLastTime) {
|
|
return;
|
|
}
|
|
_blobs.setLevel(level);
|
|
}
|
|
|
|
float64 BlobsWidget::switchConnectingProgress() const {
|
|
return _switchConnectingProgress;
|
|
}
|
|
|
|
void BlobsWidget::setSwitchConnectingProgress(float64 progress) {
|
|
_switchConnectingProgress = progress;
|
|
}
|
|
|
|
CallMuteButton::CallMuteButton(
|
|
not_null<RpWidget*> parent,
|
|
const style::CallMuteButton &st,
|
|
rpl::producer<bool> &&hideBlobs,
|
|
CallMuteButtonState initial)
|
|
: _state(initial)
|
|
, _st(&st)
|
|
, _blobs(base::make_unique_q<BlobsWidget>(
|
|
parent,
|
|
_st->active.bgSize,
|
|
rpl::combine(
|
|
rpl::single(anim::Disabled()) | rpl::then(anim::Disables()),
|
|
std::move(hideBlobs),
|
|
_state.value(
|
|
) | rpl::map([](const CallMuteButtonState &state) {
|
|
return IsInactive(state.type);
|
|
})
|
|
) | rpl::map([](bool animDisabled, bool hide, bool isBadState) {
|
|
return isBadState || !(!animDisabled && !hide);
|
|
})))
|
|
, _content(base::make_unique_q<AbstractButton>(parent))
|
|
, _colors(Colors())
|
|
, _iconState(iconStateFrom(initial.type)) {
|
|
init();
|
|
}
|
|
|
|
void CallMuteButton::refreshLabels() {
|
|
_centerLabel = base::make_unique_q<AnimatedLabel>(
|
|
_content->parentWidget(),
|
|
_state.value(
|
|
) | rpl::map([](const CallMuteButtonState &state) {
|
|
return state.subtext.isEmpty() ? state.text : QString();
|
|
}),
|
|
kSwitchLabelDuration,
|
|
_st->labelAdditional,
|
|
_st->active.label);
|
|
_label = base::make_unique_q<AnimatedLabel>(
|
|
_content->parentWidget(),
|
|
_state.value(
|
|
) | rpl::map([](const CallMuteButtonState &state) {
|
|
return state.subtext.isEmpty() ? QString() : state.text;
|
|
}),
|
|
kSwitchLabelDuration,
|
|
_st->labelAdditional,
|
|
_st->active.label);
|
|
_sublabel = base::make_unique_q<AnimatedLabel>(
|
|
_content->parentWidget(),
|
|
_state.value(
|
|
) | rpl::map([](const CallMuteButtonState &state) {
|
|
return state.subtext;
|
|
}),
|
|
kSwitchLabelDuration,
|
|
_st->labelAdditional,
|
|
_st->sublabel);
|
|
|
|
_label->show();
|
|
rpl::combine(
|
|
_content->geometryValue(),
|
|
_label->sizeValue()
|
|
) | rpl::start_with_next([=](QRect my, QSize size) {
|
|
updateLabelGeometry(my, size);
|
|
}, _label->lifetime());
|
|
_label->setAttribute(Qt::WA_TransparentForMouseEvents);
|
|
|
|
_sublabel->show();
|
|
rpl::combine(
|
|
_content->geometryValue(),
|
|
_sublabel->sizeValue()
|
|
) | rpl::start_with_next([=](QRect my, QSize size) {
|
|
updateSublabelGeometry(my, size);
|
|
}, _sublabel->lifetime());
|
|
_sublabel->setAttribute(Qt::WA_TransparentForMouseEvents);
|
|
|
|
_centerLabel->show();
|
|
rpl::combine(
|
|
_content->geometryValue(),
|
|
_centerLabel->sizeValue()
|
|
) | rpl::start_with_next([=](QRect my, QSize size) {
|
|
updateCenterLabelGeometry(my, size);
|
|
}, _centerLabel->lifetime());
|
|
_centerLabel->setAttribute(Qt::WA_TransparentForMouseEvents);
|
|
}
|
|
|
|
void CallMuteButton::refreshIcons() {
|
|
_icons[0].emplace(Lottie::IconDescriptor{
|
|
.path = u":/gui/icons/calls/voice.lottie"_q,
|
|
.color = st::groupCallIconFg,
|
|
.sizeOverride = _st->lottieSize,
|
|
.frame = (_iconState.index ? 0 : _iconState.frameTo),
|
|
});
|
|
_icons[1].emplace(Lottie::IconDescriptor{
|
|
.path = u":/gui/icons/calls/hands.lottie"_q,
|
|
.color = st::groupCallIconFg,
|
|
.sizeOverride = _st->lottieSize,
|
|
.frame = (_iconState.index ? _iconState.frameTo : 0),
|
|
});
|
|
|
|
}
|
|
|
|
auto CallMuteButton::iconStateAnimated(CallMuteButtonType previous)
|
|
-> IconState {
|
|
using Type = CallMuteButtonType;
|
|
using Key = std::pair<Type, Type>;
|
|
struct Animation {
|
|
int from = 0;
|
|
int to = 0;
|
|
};
|
|
static const auto kAnimations = std::vector<std::pair<Key, Animation>>{
|
|
{ { Type::ForceMuted, Type::Muted }, { 0, 35 } },
|
|
{ { Type::Muted, Type::Active }, { 36, 68 } },
|
|
{ { Type::Active, Type::Muted }, { 69, 98 } },
|
|
{ { Type::Muted, Type::ForceMuted }, { 99, 135 } },
|
|
{ { Type::Active, Type::ForceMuted }, { 136, 172 } },
|
|
{ { Type::ScheduledSilent, Type::ScheduledNotify }, { 173, 201 } },
|
|
{ { Type::ScheduledSilent, Type::Muted }, { 202, 236 } },
|
|
{ { Type::ScheduledSilent, Type::ForceMuted }, { 237, 273 } },
|
|
{ { Type::ScheduledNotify, Type::ForceMuted }, { 274, 310 } },
|
|
{ { Type::ScheduledNotify, Type::ScheduledSilent }, { 311, 343 } },
|
|
{ { Type::ScheduledNotify, Type::Muted }, { 344, 375 } },
|
|
{ { Type::ScheduledCanStart, Type::Muted }, { 376, 403 } },
|
|
};
|
|
static const auto kMap = [] {
|
|
// flat_multi_map_pair_type lacks some required constructors :(
|
|
auto &&list = kAnimations | ranges::views::transform([](auto &&pair) {
|
|
return base::flat_multi_map_pair_type<Key, Animation>(
|
|
pair.first,
|
|
pair.second);
|
|
});
|
|
return base::flat_map<Key, Animation>(begin(list), end(list));
|
|
}();
|
|
const auto was = TypeForIcon(previous);
|
|
const auto now = TypeForIcon(_state.current().type);
|
|
if (was == now) {
|
|
return {};
|
|
}
|
|
|
|
if (const auto i = kMap.find(Key{ was, now }); i != end(kMap)) {
|
|
return { 0, i->second.from, i->second.to };
|
|
}
|
|
return {};
|
|
}
|
|
|
|
CallMuteButton::IconState CallMuteButton::iconStateFrom(
|
|
CallMuteButtonType previous) {
|
|
if (const auto animated = iconStateAnimated(previous)) {
|
|
return animated;
|
|
}
|
|
|
|
using Type = CallMuteButtonType;
|
|
static const auto kFinal = base::flat_map<Type, int>{
|
|
{ Type::ForceMuted, 0 },
|
|
{ Type::Muted, 36 },
|
|
{ Type::Active, 69 },
|
|
{ Type::ScheduledSilent, 173 },
|
|
{ Type::ScheduledNotify, 274 },
|
|
{ Type::ScheduledCanStart, 376 },
|
|
};
|
|
|
|
const auto now = TypeForIcon(_state.current().type);
|
|
const auto i = kFinal.find(now);
|
|
|
|
Ensures(i != end(kFinal));
|
|
return { 0, i->second, i->second };
|
|
}
|
|
|
|
CallMuteButton::IconState CallMuteButton::randomWavingState() {
|
|
struct Animation {
|
|
int from = 0;
|
|
int to = 0;
|
|
};
|
|
static const auto kAnimations = std::vector<Animation>{
|
|
{ 0, 120 },
|
|
{ 120, 240 },
|
|
{ 240, 420 },
|
|
{ 420, 540 },
|
|
};
|
|
const auto index = openssl::RandomValue<uint32>() % kAnimations.size();
|
|
return { 1, kAnimations[index].from, kAnimations[index].to };
|
|
}
|
|
|
|
void CallMuteButton::init() {
|
|
refreshLabels();
|
|
refreshIcons();
|
|
|
|
const auto &button = _st->active.button;
|
|
_content->resize(button.width, button.height);
|
|
|
|
_content->events(
|
|
) | rpl::start_with_next([=](not_null<QEvent*> e) {
|
|
if (e->type() == QEvent::MouseMove) {
|
|
if (!_state.current().tooltip.isEmpty()) {
|
|
Ui::Tooltip::Show(1000, this);
|
|
}
|
|
} else if (e->type() == QEvent::Leave) {
|
|
Ui::Tooltip::Hide();
|
|
}
|
|
}, _content->lifetime());
|
|
|
|
rpl::combine(
|
|
_radialInfo.rawShowProgress.value(),
|
|
anim::Disables()
|
|
) | rpl::start_with_next([=](float64 value, bool disabled) {
|
|
auto &info = _radialInfo;
|
|
info.realShowProgress = (1. - value) / kRadialEndPartAnimation;
|
|
|
|
const auto guard = gsl::finally([&] {
|
|
_content->update();
|
|
});
|
|
|
|
if (((value == 0.) || disabled) && _radial) {
|
|
_radial->stop();
|
|
_radial = nullptr;
|
|
return;
|
|
}
|
|
if ((value > 0.) && !disabled && !_radial) {
|
|
_radial = std::make_unique<InfiniteRadialAnimation>(
|
|
[=] { _content->update(); },
|
|
_radialInfo.st);
|
|
_radial->start();
|
|
}
|
|
if ((info.realShowProgress < 1.) && !info.isDirectionToShow) {
|
|
if (_radial) {
|
|
_radial->stop(anim::type::instant);
|
|
_radial->start();
|
|
}
|
|
info.state = std::nullopt;
|
|
return;
|
|
}
|
|
|
|
if (value == 1.) {
|
|
info.state = std::nullopt;
|
|
} else {
|
|
if (_radial && !info.state.has_value()) {
|
|
info.state = _radial->computeState();
|
|
}
|
|
}
|
|
}, lifetime());
|
|
|
|
// State type.
|
|
const auto previousType =
|
|
lifetime().make_state<CallMuteButtonType>(_state.current().type);
|
|
setHandleMouseState(HandleMouseState::Disabled);
|
|
|
|
refreshGradients();
|
|
|
|
_state.value(
|
|
) | rpl::map([](const CallMuteButtonState &state) {
|
|
return state.type;
|
|
}) | rpl::start_with_next([=](CallMuteButtonType type) {
|
|
const auto previous = *previousType;
|
|
*previousType = type;
|
|
|
|
const auto mouseState = HandleMouseStateFromType(type);
|
|
setHandleMouseState(HandleMouseState::Disabled);
|
|
if (mouseState != HandleMouseState::Enabled) {
|
|
setHandleMouseState(mouseState);
|
|
}
|
|
|
|
const auto fromConnecting = IsConnecting(previous);
|
|
const auto toConnecting = IsConnecting(type);
|
|
|
|
const auto radialShowFrom = fromConnecting ? 1. : 0.;
|
|
const auto radialShowTo = toConnecting ? 1. : 0.;
|
|
|
|
const auto from = (_switchAnimation.animating() && !fromConnecting)
|
|
? (1. - _switchAnimation.value(0.))
|
|
: 0.;
|
|
const auto to = 1.;
|
|
|
|
_radialInfo.isDirectionToShow = fromConnecting;
|
|
|
|
scheduleIconState(iconStateFrom(previous));
|
|
|
|
auto callback = [=](float64 value) {
|
|
const auto brushProgress = fromConnecting ? 1. : value;
|
|
_blobs->setBlobBrush(QBrush(
|
|
_linearGradients.gradient(previous, type, brushProgress)));
|
|
_blobs->setGlowBrush(QBrush(
|
|
_glowGradients.gradient(previous, type, value)));
|
|
_blobs->update();
|
|
|
|
const auto radialShowProgress = (radialShowFrom == radialShowTo)
|
|
? radialShowTo
|
|
: anim::interpolateF(radialShowFrom, radialShowTo, value);
|
|
if (radialShowProgress != _radialInfo.rawShowProgress.current()) {
|
|
_radialInfo.rawShowProgress = radialShowProgress;
|
|
_blobs->setSwitchConnectingProgress(Clamp(
|
|
radialShowProgress / kBlobsWidgetPartAnimation));
|
|
}
|
|
|
|
overridesColors(previous, type, value);
|
|
|
|
if (value == to) {
|
|
setHandleMouseState(mouseState);
|
|
}
|
|
};
|
|
|
|
_switchAnimation.stop();
|
|
const auto duration = (1. - from) * ((fromConnecting || toConnecting)
|
|
? kSwitchStateFromConnectingDuration
|
|
: kSwitchStateDuration);
|
|
_switchAnimation.start(std::move(callback), from, to, duration);
|
|
}, lifetime());
|
|
|
|
// Icon rect.
|
|
_content->sizeValue(
|
|
) | rpl::start_with_next([=](QSize size) {
|
|
const auto icon = _st->lottieSize;
|
|
_muteIconRect = QRect(
|
|
(size.width() - icon.width()) / 2,
|
|
_st->lottieTop,
|
|
icon.width(),
|
|
icon.height());
|
|
}, lifetime());
|
|
|
|
// Paint.
|
|
_content->paintRequest(
|
|
) | rpl::start_with_next([=](QRect clip) {
|
|
Painter p(_content);
|
|
|
|
_icons[_iconState.index]->paint(p, _muteIconRect.x(), _muteIconRect.y());
|
|
|
|
if (_radialInfo.state.has_value() && _switchAnimation.animating()) {
|
|
const auto radialProgress = _radialInfo.realShowProgress;
|
|
|
|
auto r = *_radialInfo.state;
|
|
r.shown = 1.;
|
|
if (_radialInfo.isDirectionToShow) {
|
|
const auto to = r.arcFrom - kRadialFinishArcShift;
|
|
ComputeRadialFinish(r.arcFrom, radialProgress, to);
|
|
ComputeRadialFinish(r.arcLength, radialProgress);
|
|
} else {
|
|
r.arcLength = RadialState::kFull;
|
|
}
|
|
|
|
const auto opacity = (radialProgress > kOverlapProgressRadialHide)
|
|
? 0.
|
|
: _blobs->switchConnectingProgress();
|
|
p.setOpacity(opacity);
|
|
InfiniteRadialAnimation::Draw(
|
|
p,
|
|
r,
|
|
_st->active.bgPosition,
|
|
QSize(_st->active.bgSize, _st->active.bgSize),
|
|
_content->width(),
|
|
QPen(_radialInfo.st.color),
|
|
_radialInfo.st.thickness);
|
|
} else if (_radial) {
|
|
auto state = _radial->computeState();
|
|
state.shown = 1.;
|
|
|
|
InfiniteRadialAnimation::Draw(
|
|
p,
|
|
std::move(state),
|
|
_st->active.bgPosition,
|
|
QSize(_st->active.bgSize, _st->active.bgSize),
|
|
_content->width(),
|
|
QPen(_radialInfo.st.color),
|
|
_radialInfo.st.thickness);
|
|
}
|
|
}, _content->lifetime());
|
|
}
|
|
|
|
void CallMuteButton::refreshGradients() {
|
|
const auto blobsInner = [&] {
|
|
// The point of the circle at 45 degrees.
|
|
const auto w = _blobs->innerRect().width();
|
|
const auto mF = (1 - std::cos(M_PI / 4.)) * (w / 2.);
|
|
return _blobs->innerRect().marginsRemoved(QMarginsF(mF, mF, mF, mF));
|
|
}();
|
|
|
|
_linearGradients = anim::linear_gradients<CallMuteButtonType>(
|
|
_colors,
|
|
QPointF(blobsInner.x() + blobsInner.width(), blobsInner.y()),
|
|
QPointF(blobsInner.x(), blobsInner.y() + blobsInner.height()));
|
|
|
|
auto glowColors = [&] {
|
|
auto copy = _colors;
|
|
for (auto &[type, stops] : copy) {
|
|
auto firstColor = IsInactive(type)
|
|
? st::groupCallBg->c
|
|
: stops.stops[(stops.stops.size() - 1) / 2].second;
|
|
firstColor.setAlpha(kGlowAlpha);
|
|
stops.stops = QGradientStops{
|
|
{ 0., std::move(firstColor) },
|
|
{ 1., QColor(Qt::transparent) }
|
|
};
|
|
}
|
|
return copy;
|
|
}();
|
|
_glowGradients = anim::radial_gradients<CallMuteButtonType>(
|
|
std::move(glowColors),
|
|
blobsInner.center(),
|
|
_blobs->width() / 2);
|
|
}
|
|
|
|
void CallMuteButton::scheduleIconState(const IconState &state) {
|
|
if (_iconState != state) {
|
|
if (_icons[_iconState.index]->animating()) {
|
|
_scheduledState = state;
|
|
} else {
|
|
startIconState(state);
|
|
}
|
|
} else if (_scheduledState) {
|
|
_scheduledState = std::nullopt;
|
|
}
|
|
}
|
|
|
|
void CallMuteButton::startIconState(const IconState &state) {
|
|
_iconState = state;
|
|
_scheduledState = std::nullopt;
|
|
_icons[_iconState.index]->animate(
|
|
[=] { iconAnimationCallback(); },
|
|
_iconState.frameFrom,
|
|
_iconState.frameTo);
|
|
}
|
|
|
|
void CallMuteButton::iconAnimationCallback() {
|
|
_content->update(_muteIconRect);
|
|
if (!_icons[_iconState.index]->animating() && _scheduledState) {
|
|
startIconState(*_scheduledState);
|
|
}
|
|
}
|
|
|
|
QString CallMuteButton::tooltipText() const {
|
|
return _state.current().tooltip;
|
|
}
|
|
|
|
QPoint CallMuteButton::tooltipPos() const {
|
|
return QCursor::pos();
|
|
}
|
|
|
|
bool CallMuteButton::tooltipWindowActive() const {
|
|
return Ui::AppInFocus()
|
|
&& Ui::InFocusChain(_content->window())
|
|
&& _content->mapToGlobal(_content->rect()).contains(QCursor::pos());
|
|
}
|
|
|
|
const style::Tooltip *CallMuteButton::tooltipSt() const {
|
|
return &st::groupCallTooltip;
|
|
}
|
|
|
|
void CallMuteButton::updateLabelsGeometry() {
|
|
updateLabelGeometry(_content->geometry(), _label->size());
|
|
updateCenterLabelGeometry(_content->geometry(), _centerLabel->size());
|
|
updateSublabelGeometry(_content->geometry(), _sublabel->size());
|
|
}
|
|
|
|
void CallMuteButton::updateLabelGeometry(QRect my, QSize size) {
|
|
const auto skip = _st->sublabelSkip + _st->labelsSkip;
|
|
const auto contentHeight = _label->contentHeight();
|
|
const auto contentTop = my.y() + my.height() - contentHeight - skip;
|
|
_label->moveToLeft(
|
|
my.x() + (my.width() - size.width()) / 2 + _labelShakeShift,
|
|
contentTop - (size.height() - contentHeight) / 2,
|
|
my.width());
|
|
}
|
|
|
|
void CallMuteButton::updateCenterLabelGeometry(QRect my, QSize size) {
|
|
const auto skip = (_st->sublabelSkip / 2) + _st->labelsSkip;
|
|
const auto contentHeight = _centerLabel->contentHeight();
|
|
const auto contentTop = my.y() + my.height() - contentHeight - skip;
|
|
_centerLabel->moveToLeft(
|
|
my.x() + (my.width() - size.width()) / 2 + _labelShakeShift,
|
|
contentTop - (size.height() - contentHeight) / 2,
|
|
my.width());
|
|
}
|
|
|
|
void CallMuteButton::updateSublabelGeometry(QRect my, QSize size) {
|
|
const auto skip = _st->labelsSkip;
|
|
const auto contentHeight = _sublabel->contentHeight();
|
|
const auto contentTop = my.y() + my.height() - contentHeight - skip;
|
|
_sublabel->moveToLeft(
|
|
my.x() + (my.width() - size.width()) / 2 + _labelShakeShift,
|
|
contentTop - (size.height() - contentHeight) / 2,
|
|
my.width());
|
|
}
|
|
|
|
void CallMuteButton::shake() {
|
|
if (_shakeAnimation.animating()) {
|
|
return;
|
|
}
|
|
const auto update = [=] {
|
|
const auto fullProgress = _shakeAnimation.value(1.) * 6;
|
|
const auto segment = std::clamp(int(std::floor(fullProgress)), 0, 5);
|
|
const auto part = fullProgress - segment;
|
|
const auto from = (segment == 0)
|
|
? 0.
|
|
: (segment == 1 || segment == 3 || segment == 5)
|
|
? 1.
|
|
: -1.;
|
|
const auto to = (segment == 0 || segment == 2 || segment == 4)
|
|
? 1.
|
|
: (segment == 1 || segment == 3)
|
|
? -1.
|
|
: 0.;
|
|
const auto shift = from * (1. - part) + to * part;
|
|
_labelShakeShift = int(std::round(shift * st::shakeShift));
|
|
updateLabelsGeometry();
|
|
};
|
|
_shakeAnimation.start(
|
|
update,
|
|
0.,
|
|
1.,
|
|
kShiftDuration);
|
|
}
|
|
|
|
CallMuteButton::HandleMouseState CallMuteButton::HandleMouseStateFromType(
|
|
CallMuteButtonType type) {
|
|
switch (type) {
|
|
case CallMuteButtonType::Active:
|
|
case CallMuteButtonType::Muted:
|
|
return HandleMouseState::Enabled;
|
|
case CallMuteButtonType::Connecting:
|
|
return HandleMouseState::Disabled;
|
|
case CallMuteButtonType::ScheduledCanStart:
|
|
case CallMuteButtonType::ScheduledNotify:
|
|
case CallMuteButtonType::ScheduledSilent:
|
|
case CallMuteButtonType::ForceMuted:
|
|
case CallMuteButtonType::RaisedHand:
|
|
return HandleMouseState::Enabled;
|
|
}
|
|
Unexpected("Type in HandleMouseStateFromType.");
|
|
}
|
|
|
|
void CallMuteButton::setStyle(const style::CallMuteButton &st) {
|
|
if (_st == &st) {
|
|
return;
|
|
}
|
|
_st = &st;
|
|
const auto &button = _st->active.button;
|
|
_content->resize(button.width, button.height);
|
|
_blobs->setDiameter(_st->active.bgSize);
|
|
|
|
refreshIcons();
|
|
refreshLabels();
|
|
updateLabelsGeometry();
|
|
refreshGradients();
|
|
}
|
|
|
|
void CallMuteButton::setState(const CallMuteButtonState &state) {
|
|
_state = state;
|
|
}
|
|
|
|
void CallMuteButton::setLevel(float level) {
|
|
_level = level;
|
|
_blobs->setLevel(level);
|
|
}
|
|
|
|
rpl::producer<Qt::MouseButton> CallMuteButton::clicks() {
|
|
return _content->clicks() | rpl::before_next([=] {
|
|
const auto type = _state.current().type;
|
|
if (type == CallMuteButtonType::ForceMuted
|
|
|| type == CallMuteButtonType::RaisedHand) {
|
|
scheduleIconState(randomWavingState());
|
|
}
|
|
});
|
|
}
|
|
|
|
QSize CallMuteButton::innerSize() const {
|
|
return innerGeometry().size();
|
|
}
|
|
|
|
QRect CallMuteButton::innerGeometry() const {
|
|
const auto &skip = _st->active.outerRadius;
|
|
return QRect(
|
|
_content->x(),
|
|
_content->y(),
|
|
_content->width() - 2 * skip,
|
|
_content->width() - 2 * skip);
|
|
}
|
|
|
|
void CallMuteButton::moveInner(QPoint position) {
|
|
const auto &skip = _st->active.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) {
|
|
_centerLabel->setVisible(visible);
|
|
_label->setVisible(visible);
|
|
_sublabel->setVisible(visible);
|
|
_content->setVisible(visible);
|
|
_blobs->setVisible(visible);
|
|
}
|
|
|
|
bool CallMuteButton::isHidden() const {
|
|
return _content->isHidden();
|
|
}
|
|
|
|
void CallMuteButton::raise() {
|
|
_blobs->raise();
|
|
_content->raise();
|
|
_centerLabel->raise();
|
|
_label->raise();
|
|
_sublabel->raise();
|
|
}
|
|
|
|
void CallMuteButton::lower() {
|
|
_centerLabel->lower();
|
|
_label->lower();
|
|
_sublabel->lower();
|
|
_content->lower();
|
|
_blobs->lower();
|
|
}
|
|
|
|
void CallMuteButton::setHandleMouseState(HandleMouseState state) {
|
|
if (_handleMouseState == state) {
|
|
return;
|
|
}
|
|
_handleMouseState = state;
|
|
const auto handle = (_handleMouseState != HandleMouseState::Disabled);
|
|
const auto pointer = (_handleMouseState == HandleMouseState::Enabled);
|
|
_content->setAttribute(Qt::WA_TransparentForMouseEvents, !handle);
|
|
_content->setPointerCursor(pointer);
|
|
}
|
|
|
|
void CallMuteButton::overridesColors(
|
|
CallMuteButtonType fromType,
|
|
CallMuteButtonType toType,
|
|
float64 progress) {
|
|
const auto toInactive = IsInactive(toType);
|
|
const auto fromInactive = IsInactive(fromType);
|
|
if (toInactive && (progress == 1)) {
|
|
_colorOverrides = CallButtonColors();
|
|
return;
|
|
}
|
|
const auto &fromStops = _colors.find(fromType)->second.stops;
|
|
const auto &toStops = _colors.find(toType)->second.stops;
|
|
auto from = fromStops[(fromStops.size() - 1) / 2].second;
|
|
auto to = toStops[(toStops.size() - 1) / 2].second;
|
|
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 = CallButtonColors{ resultBg, resultRipple };
|
|
}
|
|
|
|
rpl::producer<CallButtonColors> CallMuteButton::colorOverrides() const {
|
|
return _colorOverrides.value();
|
|
}
|
|
|
|
not_null<RpWidget*> CallMuteButton::outer() const {
|
|
return _content.get();
|
|
}
|
|
|
|
rpl::lifetime &CallMuteButton::lifetime() {
|
|
return _blobs->lifetime();
|
|
}
|
|
|
|
CallMuteButton::~CallMuteButton() = default;
|
|
|
|
} // namespace Ui
|