Added rate call screen with stars to call panel.

This commit is contained in:
23rd
2026-08-28 12:14:11 +03:00
parent fca6099508
commit 34ce5547cb
17 changed files with 1002 additions and 42 deletions

View File

@@ -478,6 +478,8 @@ PRIVATE
calls/calls_panel.h
calls/calls_panel_background.cpp
calls/calls_panel_background.h
calls/calls_rate_call.cpp
calls/calls_rate_call.h
calls/calls_signal_bars.cpp
calls/calls_signal_bars.h
calls/calls_top_bar.cpp

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 371 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 720 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@@ -6440,6 +6440,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_call_rate_label" = "Please rate the quality of your call";
"lng_call_rate_comment" = "Comment (optional)";
"lng_call_rate_title" = "Rate this call";
"lng_call_rate_description" = "Please rate the quality of this call.";
"lng_call_ended_title" = "Call ended";
"lng_call_start" = "Start Call";
"lng_call_start_video" = "Start Video";

View File

@@ -29,6 +29,7 @@
<file alias="writing.tgs">../../animations/writing.tgs</file>
<file alias="hours.tgs">../../animations/hours.tgs</file>
<file alias="phone.tgs">../../animations/phone.tgs</file>
<file alias="call_rate.tgs">../../animations/call_rate.tgs</file>
<file alias="chat_link.tgs">../../animations/chat_link.tgs</file>
<file alias="diamond.tgs">../../animations/diamond.tgs</file>
<file alias="collectible_username.tgs">../../animations/collectible_username.tgs</file>

View File

@@ -455,6 +455,46 @@ callRatingComment: InputField(defaultInputField) {
}
callRatingCommentTop: 8px;
callRateWidth: 300px;
callRateHeight: 152px;
callRateRadius: 28px;
callRateSkip: 16px;
callRateTitleTop: 24px;
callRateDescriptionTop: 50px;
callRateStarsTop: 90px;
callRateStarsHeight: 100px;
callRateStarSize: 37px;
callRateStarSkip: 4px;
callRateBurstSize: 133px;
callRateShowShift: 24px;
callRateStarShowShift: 30px;
callRateTitle: FlatLabel(defaultFlatLabel) {
minWidth: 40px;
textFg: callNameFg;
align: align(top);
style: TextStyle(defaultTextStyle) {
font: font(15px semibold);
}
}
callRateDescription: FlatLabel(callRateTitle) {
style: TextStyle(defaultTextStyle) {
font: font(15px);
}
}
callRateStarRipple: RippleAnimation(defaultRippleAnimation) {
color: callMuteRipple;
}
callRateEndedIcon: icon {{ "calls/call_ended", callNameFg }};
callRateEndedSkip: 4px;
callRateCloseWidth: 358px;
callRateCloseHeight: 52px;
callRateCloseRadius: 8px;
callRateCloseSkip: 24px;
callRateCloseFont: font(18px semibold);
callRateCloseRipple: RippleAnimation(defaultRippleAnimation) {
color: callIconActiveRipple;
}
callDebugPadding: margins(24px, 0px, 24px, 0px);
callDebugLabel: FlatLabel(defaultFlatLabel) {
margin: callDebugPadding;

View File

@@ -622,7 +622,78 @@ crl::time Call::getDurationMs() const {
return _startTime ? (crl::now() - _startTime) : 0;
}
void Call::takeRatingToPanel() {
Expects(_ratingRequested);
_ratingInPanel = true;
}
void Call::setRating(int rating) {
_rating = rating;
}
void Call::finishRating() {
if (!_ratingInPanel) {
return;
}
_ratingInPanel = false;
_ratingRequested = false;
if (_rating > 0 && _id && _accessHash) {
const auto session = &_user->session();
session->api().request(MTPphone_SetCallRating(
MTP_flags(0),
MTP_inputPhoneCall(
MTP_long(_id),
MTP_long(_accessHash)),
MTP_int(_rating),
MTP_string()
)).done([=](const MTPUpdates &updates) {
session->api().applyUpdates(updates);
}).send();
}
_delegate->callFinished(this);
}
void Call::showRatingBox() {
Expects(_ratingRequested);
_ratingRequested = false;
const auto window = Core::App().windowFor(::Window::SeparateId(_user));
const auto session = &_user->session();
const auto callId = _id;
const auto callAccessHash = _accessHash;
auto owned = Box<Ui::RateCallBox>(Core::App().settings().sendSubmitWay());
const auto box = window
? window->show(std::move(owned))
: Ui::show(std::move(owned));
const auto sender = box->lifetime().make_state<MTP::Sender>(
&session->mtp());
box->sends(
) | rpl::take(
1 // Instead of keeping requestId.
) | rpl::on_next([=](const Ui::RateCallBox::Result &r) {
sender->request(MTPphone_SetCallRating(
MTP_flags(0),
MTP_inputPhoneCall(
MTP_long(callId),
MTP_long(callAccessHash)),
MTP_int(r.rating),
MTP_string(r.comment)
)).done([=](const MTPUpdates &updates) {
session->api().applyUpdates(updates);
box->closeBox();
}).fail([=] {
box->closeBox();
}).send();
}, box->lifetime());
}
void Call::hangup(Data::GroupCall *migrateCall, const QString &migrateSlug) {
if (_ratingInPanel) {
finishRating();
return;
}
const auto state = _state.current();
if (state == State::Busy
|| state == State::MigrationHangingUp) {
@@ -807,36 +878,10 @@ bool Call::handleUpdate(const MTPPhoneCall &call) {
}
}
if (data.is_need_rating() && _id && _accessHash) {
const auto window = Core::App().windowFor(
::Window::SeparateId(_user));
const auto session = &_user->session();
const auto callId = _id;
const auto callAccessHash = _accessHash;
auto owned = Box<Ui::RateCallBox>(
Core::App().settings().sendSubmitWay());
const auto box = window
? window->show(std::move(owned))
: Ui::show(std::move(owned));
const auto sender = box->lifetime().make_state<MTP::Sender>(
&session->mtp());
box->sends(
) | rpl::take(
1 // Instead of keeping requestId.
) | rpl::on_next([=](const Ui::RateCallBox::Result &r) {
sender->request(MTPphone_SetCallRating(
MTP_flags(0),
MTP_inputPhoneCall(
MTP_long(callId),
MTP_long(callAccessHash)),
MTP_int(r.rating),
MTP_string(r.comment)
)).done([=](const MTPUpdates &updates) {
session->api().applyUpdates(updates);
box->closeBox();
}).fail([=] {
box->closeBox();
}).send();
}, box->lifetime());
_ratingRequested = true;
}
if (const auto duration = data.vduration()) {
_discardedDuration = duration->v;
}
const auto reason = data.vreason();
if (reason
@@ -854,6 +899,9 @@ bool Call::handleUpdate(const MTPPhoneCall &call) {
} else {
setState(State::EndedByOtherDevice);
}
if (_ratingRequested && !_ratingInPanel) {
showRatingBox();
}
} return true;
case mtpc_phoneCallAccepted: {

View File

@@ -228,6 +228,19 @@ public:
crl::time getDurationMs() const;
float64 getWaitingSoundPeakValue() const;
[[nodiscard]] bool ratingRequested() const {
return _ratingRequested;
}
[[nodiscard]] bool ratingInPanel() const {
return _ratingInPanel;
}
[[nodiscard]] int discardedDuration() const {
return _discardedDuration;
}
void takeRatingToPanel();
void setRating(int rating);
void finishRating();
void applyUserConfirmation();
void answer();
void hangup(
@@ -290,6 +303,7 @@ private:
= MTP_phoneCallDiscardReasonDisconnect(),
Data::GroupCall *migrateCall = nullptr);
void finishByMigration(const QString &slug);
void showRatingBox();
void startOutgoing();
void startIncoming();
void startWaitingTrack();
@@ -362,6 +376,10 @@ private:
CallId _id = 0;
uint64 _accessHash = 0;
int _rating = 0;
int _discardedDuration = 0;
bool _ratingRequested = false;
bool _ratingInPanel = false;
uint64 _keyFingerprint = 0;
CallId _conferenceId = 0;

View File

@@ -99,6 +99,9 @@ DhConfig Instance::Delegate::getDhConfig() const {
void Instance::Delegate::callFinished(not_null<Call*> call) {
crl::on_main(call, [=] {
if (call->ratingInPanel()) {
return;
}
_instance->destroyCall(call);
});
}

View File

@@ -9,6 +9,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "boxes/peers/replace_boost_box.h" // CreateUserpicsWithMoreBadge
#include "calls/calls_panel_background.h"
#include "calls/calls_rate_call.h"
#include "data/data_photo.h"
#include "data/data_session.h"
#include "data/data_user.h"
@@ -79,6 +80,7 @@ namespace {
constexpr auto kHideControlsTimeout = 5 * crl::time(1000);
constexpr auto kHideControlsQuickTimeout = 2 * crl::time(1000);
constexpr auto kNameFadeDuration = crl::time(70);
[[nodiscard]] QByteArray BatterySvg(
const QSize &s,
@@ -332,6 +334,8 @@ void Panel::initWindow() {
_answerHangupRedial->y(),
buttonsWidth,
_answerHangupRedial->height()).contains(widgetPoint)
|| (_rateCall && _rateCall->geometry().contains(widgetPoint))
|| (_rateClose && _rateClose->geometry().contains(widgetPoint))
|| (!_outgoingPreviewInBody
&& _outgoingVideoBubble->geometry().contains(widgetPoint));
if (inControls) {
@@ -651,6 +655,16 @@ void Panel::refreshIncomingGeometry() {
void Panel::reinitWithCall(Call *call) {
_callLifetime.destroy();
_rateNameFade.stop();
_rateCall = nullptr;
_rateClose = nullptr;
_rateNameSwapped = false;
_endedDuration = 0;
_rateEndedIconPosition = QPoint();
_name->setOpacity(1.);
_bodySt = _outgoingPreviewInBody
? &st::callBodyWithPreview
: &st::callBodyLayout;
_call = call;
const auto guard = gsl::finally([&] {
updateControlsShown();
@@ -1017,10 +1031,14 @@ void Panel::showControls() {
widget()->showChildren();
_pinOnTop->setVisible(!_fullScreenOrMaximized.current());
_decline->setVisible(_decline->toggled());
_cancel->setVisible(_cancel->toggled());
_screencast->setVisible(_screencast->toggled());
_addPeople->setVisible(_addPeople->toggled());
if (_rateCall) {
hideControlsForRating();
} else {
_decline->setVisible(_decline->toggled());
_cancel->setVisible(_cancel->toggled());
_screencast->setVisible(_screencast->toggled());
_addPeople->setVisible(_addPeople->toggled());
}
const auto shown = !_incomingFrameSize.isEmpty();
_incoming->widget()->setVisible(shown);
@@ -1129,6 +1147,97 @@ void Panel::refreshOutgoingPreviewInBody(State state) {
updateControlsGeometry();
}
void Panel::showRateCall() {
Expects(_call != nullptr);
Expects(_rateCall == nullptr);
_call->takeRatingToPanel();
const auto local = int(_call->getDurationMs() / 1000);
_endedDuration = local ? local : _call->discardedDuration();
_bodySt = &st::callBodyWithPreview;
_rateCall = base::make_unique_q<RateCall>(widget());
_rateCall->ratingValue(
) | rpl::on_next([=](int rating) {
if (_call) {
_call->setRating(rating);
}
}, _rateCall->lifetime());
_rateClose = base::make_unique_q<EndCloseButton>(widget());
hideControlsForRating();
updateControlsGeometry();
updateTextColors();
updateStatusText(_call->state());
_rateCall->show();
_rateCall->showAnimated();
_rateClose->switchToClose(hangupCircleRect(), [=] { finishRateCall(); });
_rateNameFade.start([=] {
const auto value = _rateNameFade.value(1.);
if (!_rateNameSwapped && value >= 0.5) {
_rateNameSwapped = true;
_name->setText(tr::lng_call_ended_title(tr::now));
updateControlsGeometry();
}
_name->setOpacity(std::abs(2. * value - 1.));
}, 0., 1., 2 * kNameFadeDuration);
widget()->update();
}
void Panel::finishRateCall() {
if (_call) {
_call->finishRating();
}
}
void Panel::hideControlsForRating() {
_answerHangupRedial->hide();
_camera->hide();
if (_startVideo) {
_startVideo->hide();
}
_decline->hide(anim::type::instant);
_cancel->hide(anim::type::instant);
_screencast->hide(anim::type::instant);
_mute->hide(anim::type::instant);
_addPeople->hide(anim::type::instant);
if (_fingerprint) {
_fingerprint->hide();
}
if (_remoteAudioMute) {
_remoteAudioMute->hide();
}
if (_remoteLowBattery) {
_remoteLowBattery->hide();
}
hideButtonTooltip();
}
QRect Panel::hangupCircleRect() const {
const auto &st = st::callHangup;
return QRect(
_answerHangupRedial->x() + st.bgPosition.x(),
_answerHangupRedial->y() + st.bgPosition.y(),
st.bgSize,
st.bgSize);
}
QRect Panel::rateCloseRect() const {
const auto circle = hangupCircleRect();
const auto width = std::min(
st::callRateCloseWidth,
widget()->width() - 2 * st::callRateCloseSkip);
const auto height = st::callRateCloseHeight;
return QRect(
(widget()->width() - width) / 2,
circle.y() + (circle.height() - height) / 2,
width,
height);
}
void Panel::createPinOnTop() {
const auto wrap = _window->controlsWrap();
_pinOnTop = base::make_unique_q<Ui::IconButton>(
@@ -1225,6 +1334,7 @@ void Panel::updateControlsShown() {
const auto shown = !_incoming
|| _incoming->widget()->isHidden()
|| _controlsShownForce
|| _rateCall
|| !_hideControlsRequested;
if (_controlsShown != shown) {
_controlsShown = shown;
@@ -1322,7 +1432,11 @@ void Panel::updateControlsGeometry() {
st::callOutgoingPreviewMax.height(),
bodyPreviewHeightMax,
}));
const auto rateHeight = _rateCall
? (st::callRateSkip + st::callRateHeight)
: 0;
const auto contentHeight = bodyContentHeight
+ rateHeight
+ (_outgoingPreviewInBody ? bodyPreviewSize.height() : 0);
const auto remainingHeight = std::max(available - contentHeight, 0);
const auto skipHeight = remainingHeight
@@ -1351,6 +1465,14 @@ void Panel::updateControlsGeometry() {
_bodyTop + _bodySt->nameTop);
updateStatusGeometry();
if (_rateCall) {
_rateCall->setGeometry(
(widget()->width() - st::callRateWidth) / 2,
_bodyTop + bodyContentHeight + st::callRateSkip,
st::callRateWidth,
RateCall::Height());
}
if (_remoteAudioMute) {
_remoteAudioMute->moveToLeft(
(widget()->width() - _remoteAudioMute->width()) / 2,
@@ -1383,6 +1505,9 @@ void Panel::updateControlsGeometry() {
}
updateHangupGeometry();
if (_rateClose) {
_rateClose->setGeometry(rateCloseRect());
}
updateButtonTooltipGeometry();
}
@@ -1430,11 +1555,19 @@ void Panel::updateStatusGeometry() {
if (widget()->size().isEmpty()) {
return;
}
const auto &icon = st::callRateEndedIcon;
const auto showIcon = (_rateCall && _endedDuration > 0);
const auto skip = showIcon
? (icon.width() + st::callRateEndedSkip)
: 0;
_status->resizeToNaturalWidth(
widget()->width() - 2 * st::callInnerPadding);
_status->moveToLeft(
(widget()->width() - _status->width()) / 2,
_bodyTop + _bodySt->statusTop);
widget()->width() - 2 * st::callInnerPadding - skip);
const auto top = _bodyTop + _bodySt->statusTop;
const auto left = (widget()->width() - _status->width() - skip) / 2;
_status->moveToLeft(left + skip, top);
_rateEndedIconPosition = showIcon
? QPoint(left, top + (_status->height() - icon.height()) / 2)
: QPoint();
}
auto Panel::bottomButtons() const
@@ -1585,6 +1718,18 @@ void Panel::paint(QRect clip) {
}
}
if (_rateCall && !_rateEndedIconPosition.isNull()) {
const auto fg = st::callName.textFg;
const auto color = (_background
? _background->textColorOverride(fg)
: std::optional<QColor>()).value_or(fg->c);
st::callRateEndedIcon.paint(
p,
_rateEndedIconPosition,
widget()->width(),
color);
}
if (_incoming && _incoming->widget()->isHidden()) {
_call->videoIncoming()->markFrameShown();
}
@@ -1592,7 +1737,9 @@ void Panel::paint(QRect clip) {
bool Panel::handleClose() const {
if (_call) {
if (_call->state() == Call::State::WaitingUserConfirmation
if (_call->ratingInPanel()) {
_call->finishRating();
} else if (_call->state() == Call::State::WaitingUserConfirmation
|| _call->state() == Call::State::Busy
|| _call->state() == Call::State::Starting
|| _call->state() == Call::State::WaitingIncoming) {
@@ -1631,6 +1778,13 @@ void Panel::stateChanged(State state) {
const auto isWaitingUser = (state == State::WaitingUserConfirmation);
_window->togglePowerSaveBlocker(!isBusy && !isWaitingUser);
if (!_rateCall
&& _call->ratingRequested()
&& !window()->isHidden()
&& (state == State::Ended || state == State::EndedByOtherDevice)) {
showRateCall();
}
if ((state != State::HangingUp)
&& (state != State::MigrationHangingUp)
&& (state != State::Ended)
@@ -1720,6 +1874,9 @@ void Panel::refreshAnswerHangupRedialLabel() {
void Panel::updateStatusText(State state) {
auto statusText = [this, state]() -> QString {
if (_rateCall && _endedDuration > 0) {
return Ui::FormatDurationText(_endedDuration);
}
switch (state) {
case State::Starting:
case State::WaitingInit:
@@ -1757,15 +1914,31 @@ void Panel::updateStatusText(State state) {
}
void Panel::updateTextColors() {
const auto statusFg = _rateCall
? st::callName.textFg
: st::callStatus.textFg;
if (!_background) {
_name->setTextColorOverride(std::nullopt);
_status->setTextColorOverride(std::nullopt);
_status->setTextColorOverride(_rateCall
? std::make_optional(statusFg->c)
: std::nullopt);
if (_rateCall) {
_rateCall->setTextColorOverride(std::nullopt);
}
return;
}
_name->setTextColorOverride(
_background->textColorOverride(st::callName.textFg));
_status->setTextColorOverride(
_background->textColorOverride(st::callStatus.textFg));
auto status = _background->textColorOverride(statusFg);
if (_rateCall && !status) {
status = statusFg->c;
}
_status->setTextColorOverride(status);
if (_rateCall) {
_rateCall->setTextColorOverride(
_background->textColorOverride(st::callRateTitle.textFg));
_rateCall->setCardOverlay(_background->cardOverlay());
}
}
void Panel::startDurationUpdateTimer(crl::time currentDuration) {

View File

@@ -67,6 +67,8 @@ class Userpic;
class SignalBars;
class VideoBubble;
class PanelBackground;
class RateCall;
class EndCloseButton;
struct DeviceSelection;
struct ConferencePanelMigration;
@@ -158,6 +160,11 @@ private:
void refreshIncomingGeometry();
void refreshOutgoingPreviewInBody(State state);
void showRateCall();
void finishRateCall();
void hideControlsForRating();
[[nodiscard]] QRect hangupCircleRect() const;
[[nodiscard]] QRect rateCloseRect() const;
void createPinOnTop();
[[nodiscard]] QRect pinOnTopRect() const;
void toggleFullScreen(bool fullscreen);
@@ -206,6 +213,12 @@ private:
bool _buttonLabelsShown = true;
base::unique_qptr<Ui::FlatLabel> _name;
base::unique_qptr<Ui::FlatLabel> _status;
base::unique_qptr<RateCall> _rateCall;
base::unique_qptr<EndCloseButton> _rateClose;
Ui::Animations::Simple _rateNameFade;
QPoint _rateEndedIconPosition;
int _endedDuration = 0;
bool _rateNameSwapped = false;
base::unique_qptr<Ui::RpWidget> _conferenceParticipants;
base::unique_qptr<Ui::RpWidget> _fingerprint;
base::unique_qptr<Ui::PaddingWrap<Ui::FlatLabel>> _remoteAudioMute;

View File

@@ -20,6 +20,12 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "ui/top_background_gradient.h"
namespace Calls {
namespace {
constexpr auto kDarkOverlayOpacity = 0.14;
constexpr auto kDarkenAboveLightness = 0.3;
} // namespace
PanelBackground::PanelBackground(
not_null<PeerData*> peer,
@@ -217,6 +223,13 @@ std::optional<QColor> PanelBackground::textColorOverride(
return std::nullopt;
}
QColor PanelBackground::cardOverlay() const {
const auto base = edgeColor().value_or(st::callBgOpaque->c);
return (base.lightnessF() > kDarkenAboveLightness)
? QColor(0, 0, 0, anim::interpolate(0, 255, kDarkOverlayOpacity))
: st::callIconBg->c;
}
rpl::lifetime &PanelBackground::lifetime() {
return _lifetime;
}

View File

@@ -42,6 +42,8 @@ public:
[[nodiscard]] std::optional<QColor> textColorOverride(
const style::color &defaultColor) const;
[[nodiscard]] QColor cardOverlay() const;
private:
void updateBrush(
QSize widgetSize,

View File

@@ -0,0 +1,528 @@
/*
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_rate_call.h"
#include "lang/lang_keys.h"
#include "lottie/lottie_icon.h"
#include "ui/effects/ripple_animation.h"
#include "ui/painter.h"
#include "ui/rect.h"
#include "styles/style_calls.h"
#include <QtGui/QPainterPathStroker>
namespace Calls {
namespace {
constexpr auto kBurstMinRating = 4;
constexpr auto kShowDuration = crl::time(250);
constexpr auto kStarShowDelay = crl::time(16);
constexpr auto kSelectDuration = crl::time(250);
constexpr auto kCardShowScaleFrom = 0.7;
constexpr auto kStarShowScaleFrom = 0.3;
constexpr auto kStarPressedScale = 0.8;
constexpr auto kDarkOpacity = 0.14;
constexpr auto kRippleOpacity = 0.3;
constexpr auto kPathSize = 96.;
constexpr auto kPathStroke = 6.;
constexpr auto kPathMiterLimit = 4.;
constexpr auto kCloseTextShadeOpacity = 0.15;
constexpr auto kCloseRippleOpacity = 0.12;
constexpr auto kMaxBursts = 5;
struct StarPoint {
float64 x = 0.;
float64 y = 0.;
float64 inX = 0.;
float64 inY = 0.;
float64 outX = 0.;
float64 outY = 0.;
};
const StarPoint kStarPath[] = {
{ 46.080, 69.940, 0.855, -0.588, 0.000, 0.000 },
{ 32.727, 79.118, 0.000, 0.000, -2.274, 1.563 },
{ 25.770, 77.844, 1.568, 2.267, -0.860, -1.243 },
{ 25.094, 73.591, -0.432, 1.447, 0.000, 0.000 },
{ 29.725, 58.097, 0.000, 0.000, 0.296, -0.992 },
{ 28.848, 55.405, 0.825, 0.629, 0.000, 0.000 },
{ 15.963, 45.583, 0.000, 0.000, -2.194, -1.673 },
{ 15.029, 38.595, -1.678, 2.187, 0.920, -1.199 },
{ 18.879, 36.640, -1.514, 0.037, 0.000, 0.000 },
{ 35.095, 36.242, 0.000, 0.000, 1.038, -0.025 },
{ 37.392, 34.578, -0.345, 0.976, 0.000, 0.000 },
{ 42.783, 19.330, 0.000, 0.000, 0.918, -2.597 },
{ 49.162, 16.285, -2.605, -0.915, 1.429, 0.502 },
{ 52.218, 19.330, -0.503, -1.424, 0.000, 0.000 },
{ 57.608, 34.578, 0.000, 0.000, 0.345, 0.976 },
{ 59.905, 36.242, -1.038, -0.025, 0.000, 0.000 },
{ 76.121, 36.640, 0.000, 0.000, 2.762, 0.068 },
{ 80.998, 41.746, 0.068, -2.752, -0.037, 1.509 },
{ 79.037, 45.583, 1.203, -0.917, 0.000, 0.000 },
{ 66.152, 55.405, 0.000, 0.000, -0.825, 0.629 },
{ 65.275, 58.097, -0.296, -0.992, 0.000, 0.000 },
{ 69.906, 73.591, 0.000, 0.000, 0.789, 2.639 },
{ 66.541, 79.792, 2.647, -0.786, -1.452, 0.431 },
{ 62.273, 79.118, 1.247, 0.857, 0.000, 0.000 },
{ 48.920, 69.940, 0.000, 0.000, -0.855, -0.588 },
};
[[nodiscard]] QPainterPath StarPath(int size) {
const auto scale = size / kPathSize;
const auto map = [&](float64 x, float64 y) {
return QPointF(x * scale, y * scale);
};
const auto count = int(std::size(kStarPath));
auto result = QPainterPath();
result.moveTo(map(kStarPath[0].x, kStarPath[0].y));
for (auto i = 0; i != count; ++i) {
const auto &from = kStarPath[i];
const auto &to = kStarPath[(i + 1) % count];
result.cubicTo(
map(from.x + from.outX, from.y + from.outY),
map(to.x + to.inX, to.y + to.inY),
map(to.x, to.y));
}
result.closeSubpath();
return result;
}
[[nodiscard]] QImage StarImage(int size, QColor color, bool filled) {
const auto ratio = style::DevicePixelRatio();
auto result = QImage(
QSize(size, size) * ratio,
QImage::Format_ARGB32_Premultiplied);
result.setDevicePixelRatio(ratio);
result.fill(Qt::transparent);
auto p = QPainter(&result);
auto hq = PainterHighQualityEnabler(p);
const auto path = StarPath(size);
p.setPen(Qt::NoPen);
p.setBrush(color);
if (filled) {
p.drawPath(path);
} else {
auto stroker = QPainterPathStroker();
stroker.setWidth(kPathStroke * size / kPathSize);
stroker.setJoinStyle(Qt::MiterJoin);
stroker.setMiterLimit(kPathMiterLimit);
p.drawPath(stroker.createStroke(path).intersected(path));
}
p.end();
return result;
}
} // namespace
RateCall::RateCall(QWidget *parent)
: RpWidget(parent)
, _title(st::callRateTitle.minWidth)
, _description(st::callRateDescription.minWidth) {
_title.setText(st::callRateTitle.style, tr::lng_call_rate_title(tr::now));
_description.setText(
st::callRateDescription.style,
tr::lng_call_rate_description(tr::now));
_cardOverlay = QColor(0, 0, 0, anim::interpolate(0, 255, kDarkOpacity));
setMouseTracking(true);
resize(st::callRateWidth, Height());
}
RateCall::~RateCall() = default;
int RateCall::Height() {
return st::callRateStarsTop + st::callRateStarsHeight;
}
void RateCall::showAnimated() {
_showAnimation.stop();
_showAnimation = {};
_showAnimation.start(
[=] { update(); },
0.,
1.,
kShowDuration + (kStarsCount - 1) * kStarShowDelay,
anim::linear);
update();
}
void RateCall::setCardOverlay(QColor color) {
if (_cardOverlay == color) {
return;
}
_cardOverlay = color;
update();
}
void RateCall::setTextColorOverride(std::optional<QColor> color) {
if (_textColorOverride == color) {
return;
}
_textColorOverride = color;
_starOutline = _starFilled = QImage();
update();
}
int RateCall::rating() const {
return _rating.current();
}
rpl::producer<int> RateCall::ratingValue() const {
return _rating.value();
}
QColor RateCall::textColor() const {
return _textColorOverride.value_or(st::callRateTitle.textFg->c);
}
QRect RateCall::cardRect() const {
return QRect(
(width() - st::callRateWidth) / 2,
0,
st::callRateWidth,
st::callRateHeight);
}
QRect RateCall::starRect(int index) const {
const auto single = st::callRateStarSize;
const auto full = kStarsCount * single
+ (kStarsCount - 1) * st::callRateStarSkip;
const auto left = (width() - full) / 2
+ index * (single + st::callRateStarSkip);
return QRect(left, st::callRateStarsTop, single, single);
}
int RateCall::starByPosition(QPoint position) const {
for (auto i = 0; i != kStarsCount; ++i) {
if (starRect(i).contains(position)) {
return i;
}
}
return -1;
}
float64 RateCall::cardShown() const {
const auto total = kShowDuration + (kStarsCount - 1) * kStarShowDelay;
const auto passed = _showAnimation.value(1.) * total;
return anim::sineInOut(1., std::clamp(passed / kShowDuration, 0., 1.));
}
float64 RateCall::starShown(int index) const {
const auto total = kShowDuration + (kStarsCount - 1) * kStarShowDelay;
const auto passed = _showAnimation.value(1.) * total
- index * kStarShowDelay;
return anim::sineInOut(1., std::clamp(passed / kShowDuration, 0., 1.));
}
const QImage &RateCall::starImage(bool filled) const {
const auto color = textColor();
if (_starColor != color) {
_starColor = color;
_starOutline = _starFilled = QImage();
}
auto &image = filled ? _starFilled : _starOutline;
if (image.isNull()) {
image = StarImage(st::callRateStarSize, color, filled);
}
return image;
}
void RateCall::paintEvent(QPaintEvent *e) {
auto p = QPainter(this);
auto hq = PainterHighQualityEnabler(p);
paintCard(p, cardShown());
for (auto i = 0; i != kStarsCount; ++i) {
paintStar(p, i, starShown(i));
}
paintBurst(p);
}
void RateCall::paintCard(QPainter &p, float64 shown) {
if (shown <= 0.) {
return;
}
const auto card = cardRect();
const auto scale = kCardShowScaleFrom
+ (1. - kCardShowScaleFrom) * shown;
p.save();
p.setOpacity(shown);
p.translate(0, anim::interpolate(st::callRateShowShift, 0, shown));
p.translate(card.center());
p.scale(scale, scale);
p.translate(-card.center());
p.setPen(Qt::NoPen);
p.setBrush(_cardOverlay);
p.drawRoundedRect(card, st::callRateRadius, st::callRateRadius);
p.setPen(textColor());
_title.draw(p, {
.position = QPoint(card.x(), card.y() + st::callRateTitleTop),
.outerWidth = width(),
.availableWidth = card.width(),
.align = style::al_top,
});
_description.draw(p, {
.position = QPoint(card.x(), card.y() + st::callRateDescriptionTop),
.outerWidth = width(),
.availableWidth = card.width(),
.align = style::al_top,
});
p.restore();
}
void RateCall::paintStar(QPainter &p, int index, float64 shown) {
if (shown <= 0.) {
return;
}
auto &star = _stars[index];
const auto rect = starRect(index);
if (star.ripple && !star.ripple->empty()) {
auto color = textColor();
color.setAlphaF(color.alphaF() * kRippleOpacity);
p.setOpacity(shown);
star.ripple->paint(p, rect.x(), rect.y(), width(), &color);
p.setOpacity(1.);
}
const auto filled = star.filled.value(star.filledTo);
const auto scale = star.pressed.value(star.pressedTo)
* (kStarShowScaleFrom + (1. - kStarShowScaleFrom) * shown);
p.save();
p.translate(0, anim::interpolate(st::callRateStarShowShift, 0, shown));
p.translate(rect.center());
p.scale(scale, scale);
p.translate(-rect.center());
if (filled < 1.) {
p.setOpacity(shown * (1. - filled));
p.drawImage(rect.topLeft(), starImage(false));
}
if (filled > 0.) {
p.setOpacity(shown * filled);
p.drawImage(rect.topLeft(), starImage(true));
}
p.restore();
p.setOpacity(1.);
}
void RateCall::paintBurst(QPainter &p) {
const auto size = st::callRateBurstSize;
for (const auto &burst : _bursts) {
if (!burst.icon || !burst.icon->animating()) {
continue;
}
burst.icon->paint(
p,
burst.center.x() - size / 2,
burst.center.y() - size / 2);
}
}
void RateCall::setOver(int index) {
if (_over == index) {
return;
}
_over = index;
setCursor((_over >= 0) ? style::cur_pointer : style::cur_default);
}
void RateCall::applyPreview(int preview) {
if (_preview == preview) {
return;
}
_preview = preview;
const auto shown = _preview ? _preview : _rating.current();
for (auto i = 0; i != kStarsCount; ++i) {
auto &star = _stars[i];
const auto filled = (i < shown) ? 1. : 0.;
const auto pressed = (_preview && (i < _preview))
? kStarPressedScale
: 1.;
if (star.filledTo != filled) {
star.filled.start(
[=] { update(); },
star.filled.value(star.filledTo),
filled,
kSelectDuration);
star.filledTo = filled;
}
if (star.pressedTo != pressed) {
star.pressed.start(
[=] { update(); },
star.pressed.value(star.pressedTo),
pressed,
kSelectDuration);
star.pressedTo = pressed;
}
}
update();
}
void RateCall::commit(int index) {
_rating = index + 1;
applyPreview(0);
if (_rating.current() >= kBurstMinRating) {
startBurst(index);
}
}
void RateCall::startBurst(int index) {
const auto free = ranges::find_if(_bursts, [](const Burst &burst) {
return !burst.icon || !burst.icon->animating();
});
const auto burst = (free != end(_bursts))
? &*free
: (int(_bursts.size()) < kMaxBursts)
? &_bursts.emplace_back()
: &*ranges::min_element(_bursts, ranges::less(), &Burst::started);
if (!burst->icon) {
burst->icon = Lottie::MakeIcon({
.name = u"call_rate"_q,
.sizeOverride = QSize(
st::callRateBurstSize,
st::callRateBurstSize),
});
if (!burst->icon->valid()) {
burst->icon = nullptr;
return;
}
}
burst->center = starRect(index).center();
burst->started = crl::now();
const auto last = burst->icon->framesCount() - 1;
burst->icon->animate([=] { update(); }, 0, last);
update();
}
void RateCall::mouseMoveEvent(QMouseEvent *e) {
setOver(starByPosition(e->pos()));
}
void RateCall::mousePressEvent(QMouseEvent *e) {
if (e->button() != Qt::LeftButton) {
return;
}
setOver(starByPosition(e->pos()));
if (_over < 0) {
return;
}
_pressed = _over;
auto &star = _stars[_pressed];
const auto rect = starRect(_pressed);
if (!star.ripple) {
star.ripple = std::make_unique<Ui::RippleAnimation>(
st::callRateStarRipple,
Ui::RippleAnimation::EllipseMask(rect.size()),
[=] { update(); });
}
star.ripple->add(e->pos() - rect.topLeft());
applyPreview(_pressed + 1);
}
void RateCall::mouseReleaseEvent(QMouseEvent *e) {
if (e->button() != Qt::LeftButton) {
return;
}
const auto pressed = std::exchange(_pressed, -1);
if (pressed < 0) {
return;
} else if (const auto &ripple = _stars[pressed].ripple) {
ripple->lastStop();
}
setOver(starByPosition(e->pos()));
if (_over == pressed) {
commit(pressed);
} else {
applyPreview(0);
}
}
void RateCall::leaveEventHook(QEvent *e) {
setOver(-1);
}
EndCloseButton::EndCloseButton(QWidget *parent)
: RippleButton(parent, st::callRateCloseRipple)
, _text(tr::lng_close(tr::now)) {
setDisabled(true);
hide();
}
void EndCloseButton::switchToClose(QRect hangupCircle, Fn<void()> close) {
_from = hangupCircle;
setClickedCallback(std::move(close));
_animation.start([=] { update(); }, 0., 1., st::callPanelDuration);
_animation.setFinishedCallback([=] { setDisabled(false); });
show();
update();
}
void EndCloseButton::prepareFrame() {
const auto ratio = style::DevicePixelRatio();
if (_frame.size() != size() * ratio) {
_frame = QImage(size() * ratio, QImage::Format_ARGB32_Premultiplied);
_frame.setDevicePixelRatio(ratio);
}
_frame.fill(Qt::transparent);
auto p = QPainter(&_frame);
auto hq = PainterHighQualityEnabler(p);
p.setPen(Qt::NoPen);
p.setBrush(st::callIconBgActive);
p.drawRoundedRect(
rect(),
st::callRateCloseRadius,
st::callRateCloseRadius);
auto ripple = st::callIconFgActive->c;
ripple.setAlphaF(ripple.alphaF() * kCloseRippleOpacity);
paintRipple(p, 0, 0, &ripple);
p.setFont(st::callRateCloseFont);
p.setCompositionMode(QPainter::CompositionMode_DestinationOut);
p.setPen(QColor(0, 0, 0, 255));
p.drawText(rect(), Qt::AlignCenter, _text);
p.setCompositionMode(QPainter::CompositionMode_SourceOver);
auto shade = st::callIconFgActive->c;
shade.setAlphaF(shade.alphaF() * kCloseTextShadeOpacity);
p.setPen(shade);
p.drawText(rect(), Qt::AlignCenter, _text);
}
void EndCloseButton::paintEvent(QPaintEvent *e) {
auto p = QPainter(this);
const auto shown = _animation.value(1.);
if (shown < 1.) {
auto hq = PainterHighQualityEnabler(p);
const auto circle = _from.translated(-pos());
p.setOpacity(1. - shown);
p.setPen(Qt::NoPen);
p.setBrush(st::callHangupBg);
p.drawEllipse(circle);
st::callHangup.button.icon.paintInCenter(
p,
circle,
st::callIconFg->c);
}
prepareFrame();
p.setOpacity(shown);
p.drawImage(0, 0, _frame);
}
QImage EndCloseButton::prepareRippleMask() const {
return Ui::RippleAnimation::RoundRectMask(
size(),
st::callRateCloseRadius);
}
} // namespace Calls

View File

@@ -0,0 +1,116 @@
/*
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
*/
#pragma once
#include "ui/effects/animations.h"
#include "ui/rp_widget.h"
#include "ui/text/text.h"
#include "ui/widgets/buttons.h"
namespace Lottie {
class Icon;
} // namespace Lottie
namespace Ui {
class RippleAnimation;
} // namespace Ui
namespace Calls {
class RateCall final : public Ui::RpWidget {
public:
explicit RateCall(QWidget *parent);
~RateCall();
void showAnimated();
void setTextColorOverride(std::optional<QColor> color);
void setCardOverlay(QColor color);
[[nodiscard]] int rating() const;
[[nodiscard]] rpl::producer<int> ratingValue() const;
[[nodiscard]] static int Height();
private:
static constexpr auto kStarsCount = 5;
struct Burst {
std::unique_ptr<Lottie::Icon> icon;
QPoint center;
crl::time started = 0;
};
struct Star {
std::unique_ptr<Ui::RippleAnimation> ripple;
Ui::Animations::Simple filled;
Ui::Animations::Simple pressed;
float64 filledTo = 0.;
float64 pressedTo = 1.;
};
void paintEvent(QPaintEvent *e) override;
void mouseMoveEvent(QMouseEvent *e) override;
void mousePressEvent(QMouseEvent *e) override;
void mouseReleaseEvent(QMouseEvent *e) override;
void leaveEventHook(QEvent *e) override;
void paintCard(QPainter &p, float64 shown);
void paintStar(QPainter &p, int index, float64 shown);
void paintBurst(QPainter &p);
void setOver(int index);
void applyPreview(int preview);
void commit(int index);
void startBurst(int index);
[[nodiscard]] QRect cardRect() const;
[[nodiscard]] QRect starRect(int index) const;
[[nodiscard]] int starByPosition(QPoint position) const;
[[nodiscard]] QColor textColor() const;
[[nodiscard]] float64 cardShown() const;
[[nodiscard]] float64 starShown(int index) const;
[[nodiscard]] const QImage &starImage(bool filled) const;
Ui::Text::String _title;
Ui::Text::String _description;
std::array<Star, kStarsCount> _stars;
std::vector<Burst> _bursts;
Ui::Animations::Simple _showAnimation;
std::optional<QColor> _textColorOverride;
QColor _cardOverlay;
mutable QImage _starOutline;
mutable QImage _starFilled;
mutable QColor _starColor;
rpl::variable<int> _rating = 0;
int _preview = 0;
int _over = -1;
int _pressed = -1;
};
class EndCloseButton final : public Ui::RippleButton {
public:
explicit EndCloseButton(QWidget *parent);
void switchToClose(QRect hangupCircle, Fn<void()> close);
private:
void paintEvent(QPaintEvent *e) override;
QImage prepareRippleMask() const override;
void prepareFrame();
const QString _text;
QRect _from;
Ui::Animations::Simple _animation;
QImage _frame;
};
} // namespace Calls