diff --git a/Telegram/SourceFiles/boxes/polls.style b/Telegram/SourceFiles/boxes/polls.style index b5551e8042..f0013689a1 100644 --- a/Telegram/SourceFiles/boxes/polls.style +++ b/Telegram/SourceFiles/boxes/polls.style @@ -9,6 +9,7 @@ using "ui/basic.style"; using "ui/widgets/widgets.style"; using "boxes/boxes.style"; +using "chat_helpers/chat_helpers.style"; historyPollQuestionFont: semiboldFont; historyPollQuestionStyle: TextStyle(defaultTextStyle) { @@ -22,7 +23,8 @@ historyPollMediaSideSkip: 1px; historyPollMediaSkip: 10px; historyPollDescriptionSkip: 8px; historyPollSubtitleSkip: 4px; -historyPollAnswerPadding: margins(32px, 10px, 0px, 10px); +historyPollAnswerPadding: margins(32px, 6px, 0px, 6px); +historyPollAnswerPaddingNoMedia: margins(32px, 10px, 0px, 10px); historyPollAnswersSkip: 2px; historyPollPercentFont: semiboldFont; historyPollPercentSkip: 5px; @@ -30,8 +32,10 @@ historyPollPercentTop: 0px; historyPollTotalVotesSkip: 5px; historyPollFillingMin: 4px; historyPollFillingHeight: 4px; -historyPollFillingRadius: 1px; +historyPollFillingRadius: 2px; +historyPollFillingBgOpacity: 0.15; historyPollFillingBottom: 2px; +historyPollFillingTop: 6px; historyPollFillingRight: 4px; historyPollRadio: Radio(defaultRadio) { bg: transparent; @@ -43,6 +47,7 @@ historyPollRadio: Radio(defaultRadio) { duration: universalDuration; rippleAreaPadding: 8px; } +historyPollCheckboxRadius: 3px; historyPollRadioOpacity: 0.7; historyPollRadioOpacityOver: 1.; historyPollDuration: 300; @@ -54,6 +59,13 @@ historyPollRippleOpacity: 0.3; historyPollRecentVotersSkip: 4px; historyPollRecentVoterSize: 18px; historyPollRecentVoterSkip: 13px; +historyPollAnswerUserpics: GroupCallUserpics { + size: 16px; + shift: 5px; + stroke: 1px; + align: align(left); +} +historyPollAnswerUserpicsSkip: 4px; historyPollBottomButtonSkip: 15px; historyPollBottomButtonTop: 4px; historyPollExplanationSkip: 8px; diff --git a/Telegram/SourceFiles/history/view/media/history_view_poll.cpp b/Telegram/SourceFiles/history/view/media/history_view_poll.cpp index 57ef4e6ebf..3c2b61a644 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_poll.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_poll.cpp @@ -32,9 +32,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/effects/fireworks_animation.h" #include "ui/toast/toast.h" #include "ui/painter.h" +#include "ui/rect.h" #include "ui/dynamic_image.h" #include "ui/dynamic_thumbnails.h" #include "history/view/media/history_view_media_common.h" +#include "history/view/history_view_group_call_bar.h" #include "data/data_media_types.h" #include "data/data_document.h" #include "data/data_photo.h" @@ -50,6 +52,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "api/api_polls.h" #include "window/window_session_controller.h" #include "styles/style_chat.h" +#include "styles/style_chat_helpers.h" #include "styles/style_widgets.h" #include "styles/style_window.h" @@ -320,6 +323,8 @@ struct Poll::Answer { int votesPercentWidth = 0; float64 filling = 0.; QString votesPercentString; + QString votesCountString; + int votesCountWidth = 0; bool chosen = false; bool correct = false; bool selected = false; @@ -331,6 +336,8 @@ struct Poll::Answer { PollThumbnailKind thumbnailKind = PollThumbnailKind::None; uint64 thumbnailId = 0; mutable std::unique_ptr ripple; + std::vector recentVoters; + mutable QImage recentVotersImage; }; struct Poll::AttachedMedia { @@ -476,9 +483,12 @@ QSize Poll::countOptimalSize() { _answers ) | ranges::views::transform([](const Answer &answer) { const auto media = answer.thumbnail ? PollAnswerMediaSize() : 0; - return st::historyPollAnswerPadding.top() + const auto &padding = answer.thumbnail + ? st::historyPollAnswerPadding + : st::historyPollAnswerPaddingNoMedia; + return padding.top() + std::max(answer.text.minHeight(), media) - + st::historyPollAnswerPadding.bottom(); + + padding.bottom(); }), 0); const auto bottomButtonHeight = inlineFooter() @@ -563,9 +573,12 @@ int Poll::countAnswerHeight( int innerWidth) const { const auto media = answer.thumbnail ? PollAnswerMediaSize() : 0; const auto textWidth = countAnswerContentWidth(answer, innerWidth); - return st::historyPollAnswerPadding.top() + const auto &padding = answer.thumbnail + ? st::historyPollAnswerPadding + : st::historyPollAnswerPaddingNoMedia; + return padding.top() + std::max(answer.text.countHeight(textWidth), media) - + st::historyPollAnswerPadding.bottom(); + + padding.bottom(); } QSize Poll::countCurrentSize(int newWidth) { @@ -1022,6 +1035,9 @@ void Poll::updateAnswers() { answer.fillData(_poll, *i, context); answer.fillMedia(_poll, *i, messageContext, repaintThumbnail); } + _anyAnswerHasMedia = ranges::any_of(_answers, [](const Answer &a) { + return a.thumbnail != nullptr; + }); return; } _answers = ranges::views::all(options) | ranges::views::transform([&]( @@ -1050,6 +1066,9 @@ void Poll::updateAnswers() { for (auto &answer : _answers) { answer.handler = createAnswerClickHandler(answer); } + _anyAnswerHasMedia = ranges::any_of(_answers, [](const Answer &a) { + return a.thumbnail != nullptr; + }); resetAnswersAnimation(); } @@ -1191,6 +1210,36 @@ void Poll::updateAnswerVotesFromOriginal( } answer.votes = original.votes; answer.filling = answer.votes / float64(maxVotes); + if (showVotes() && answer.votes) { + answer.votesCountString = QString::number(answer.votes); + answer.votesCountWidth = st::normalFont->width( + answer.votesCountString); + } else { + answer.votesCountString.clear(); + answer.votesCountWidth = 0; + } + const auto changed = !ranges::equal( + answer.recentVoters, + original.recentVoters, + ranges::equal_to(), + &UserpicInRow::peer); + if (changed) { + auto updated = std::vector(); + updated.reserve(original.recentVoters.size()); + for (const auto &peer : original.recentVoters) { + const auto i = ranges::find( + answer.recentVoters, + peer, + &UserpicInRow::peer); + if (i != end(answer.recentVoters)) { + updated.push_back(std::move(*i)); + } else { + updated.push_back({ .peer = peer }); + } + } + answer.recentVoters = std::move(updated); + answer.recentVotersImage = QImage(); + } } void Poll::updateAnswerVotes() { @@ -1384,15 +1433,13 @@ void Poll::paintInlineFooter( const PaintContext &context) const { const auto stm = context.messageStyle(); p.setPen(stm->msgDateFg); + const auto labelWidth = _totalVotesLabel.maxWidth(); + const auto labelLeft = left + (paintw - labelWidth) / 2; _totalVotesLabel.drawLeftElided( p, - left, + labelLeft, top, - _parent->data()->reactions().empty() - ? std::min( - _totalVotesLabel.maxWidth(), - paintw - _parent->bottomInfoFirstLineWidth()) - : _totalVotesLabel.maxWidth(), + labelWidth, width()); } @@ -1408,7 +1455,14 @@ void Poll::paintBottom( const auto stm = context.messageStyle(); if (showVotersCount()) { p.setPen(stm->msgDateFg); - _totalVotesLabel.draw(p, left, stringtop, paintw, style::al_top); + const auto labelWidth = _totalVotesLabel.maxWidth(); + const auto labelLeft = left + (paintw - labelWidth) / 2; + _totalVotesLabel.draw( + p, + labelLeft, + stringtop, + labelWidth, + style::al_top); } else { const auto link = showVotes() ? _showResultsLink @@ -1713,12 +1767,19 @@ int Poll::paintAnswer( const PaintContext &context) const { const auto height = countAnswerHeight(answer, width); const auto stm = context.messageStyle(); + const auto &answerPadding = answer.thumbnail + ? st::historyPollAnswerPadding + : st::historyPollAnswerPaddingNoMedia; const auto aleft = left + st::historyPollAnswerPadding.left(); const auto awidth = width - st::historyPollAnswerPadding.left() - st::historyPollAnswerPadding.right(); const auto media = answer.thumbnail ? PollAnswerMediaSize() : 0; const auto textWidth = countAnswerContentWidth(answer, width); + const auto anyMediaWidth = _anyAnswerHasMedia + ? (PollAnswerMediaSize() + PollAnswerMediaSkip()) + : 0; + const auto barContentWidth = std::max(1, awidth - anyMediaWidth); if (answer.ripple) { p.setOpacity(st::historyPollRippleOpacity); @@ -1734,6 +1795,57 @@ int Poll::paintAnswer( p.setOpacity(1.); } + const auto paintVotesCount = [&](float64 opacity = 1.) { + if (answer.votesCountString.isEmpty()) { + return; + } + if (!answer.recentVoters.empty()) { + if (NeedRegenerateUserpics( + answer.recentVotersImage, + answer.recentVoters)) { + const auto was = !answer.recentVotersImage.isNull(); + GenerateUserpicsInRow( + answer.recentVotersImage, + answer.recentVoters, + st::historyPollAnswerUserpics); + if (!was) { + history()->owner().registerHeavyViewPart( + _parent); + } + } + } + const auto userpicsWidth = answer.recentVotersImage.isNull() + ? 0 + : (answer.recentVotersImage.width() + / style::DevicePixelRatio()); + const auto userpicsExtra = userpicsWidth + ? (st::historyPollAnswerUserpicsSkip + userpicsWidth) + : 0; + const auto rightEdge = aleft + awidth + - anyMediaWidth + - st::historyPollFillingRight; + const auto countX = rightEdge + - answer.votesCountWidth + - userpicsExtra; + const auto atop = top + answerPadding.top(); + p.setOpacity(opacity); + p.setFont(st::normalFont); + p.setPen(stm->msgDateFg); + p.drawTextLeft( + countX, + atop, + outerWidth, + answer.votesCountString, + answer.votesCountWidth); + if (userpicsWidth) { + const auto userpicsX = rightEdge - userpicsWidth; + const auto userpicsY = atop + + (st::normalFont->height + - st::historyPollAnswerUserpics.size) / 2; + p.drawImage(userpicsX, userpicsY, answer.recentVotersImage); + } + }; + if (animation) { const auto opacity = animation->opacity.current(); if (opacity < 1.) { @@ -1752,8 +1864,10 @@ int Poll::paintAnswer( percentWidth, left, top, + answerPadding.top(), outerWidth, context); + paintVotesCount(opacity); p.setOpacity(sqrt(opacity)); paintFilling( p, @@ -1762,9 +1876,9 @@ int Poll::paintAnswer( animation->filling.current(), left, top, + answerPadding.top(), width, - textWidth, - height, + barContentWidth, context); p.setOpacity(1.); } @@ -1777,8 +1891,10 @@ int Poll::paintAnswer( answer.votesPercentWidth, left, top, + answerPadding.top(), outerWidth, context); + paintVotesCount(); paintFilling( p, answer.chosen, @@ -1786,13 +1902,13 @@ int Poll::paintAnswer( answer.filling, left, top, + answerPadding.top(), width, - textWidth, - height, + barContentWidth, context); } - top += st::historyPollAnswerPadding.top(); + top += answerPadding.top(); if (answer.thumbnail) { const auto target = QRect( aleft + awidth - media, @@ -1840,7 +1956,10 @@ void Poll::paintRadio( int left, int top, const PaintContext &context) const { - top += st::historyPollAnswerPadding.top(); + const auto &answerPadding = answer.thumbnail + ? st::historyPollAnswerPadding + : st::historyPollAnswerPaddingNoMedia; + top += answerPadding.top(); const auto stm = context.messageStyle(); @@ -1857,7 +1976,9 @@ void Poll::paintRadio( p.setOpacity(o * (over ? st::historyPollRadioOpacityOver : st::historyPollRadioOpacity)); } + const auto multiChoice = (_flags & PollData::Flag::MultiChoice); const auto rect = QRectF(left, top, radio.diameter, radio.diameter).marginsRemoved(QMarginsF(radio.thickness / 2., radio.thickness / 2., radio.thickness / 2., radio.thickness / 2.)); + const auto radius = st::historyPollCheckboxRadius; if (_sendingAnimation && _sendingAnimation->option == answer.option) { const auto &active = stm->msgServiceFg; if (anim::Disabled()) { @@ -1868,17 +1989,25 @@ void Poll::paintRadio( pen.setWidth(radio.thickness); pen.setCapStyle(Qt::RoundCap); p.setPen(pen); - p.drawArc( - rect, - state.arcFrom, - state.arcLength); + if (multiChoice) { + p.drawRoundedRect(rect, radius, radius); + } else { + p.drawArc( + rect, + state.arcFrom, + state.arcLength); + } } } else { if (checkmark < 1.) { auto pen = regular->p; pen.setWidth(radio.thickness); p.setPen(pen); - p.drawEllipse(rect); + if (multiChoice) { + p.drawRoundedRect(rect, radius, radius); + } else { + p.drawEllipse(rect); + } } if (checkmark > 0.) { const auto removeFull = (radio.diameter / 2 - radio.thickness); @@ -1888,7 +2017,12 @@ void Poll::paintRadio( pen.setWidth(radio.thickness); p.setPen(pen); p.setBrush(color); - p.drawEllipse(rect.marginsRemoved({ removeNow, removeNow, removeNow, removeNow })); + const auto inner = rect - Margins(removeNow); + if (multiChoice) { + p.drawRoundedRect(inner, radius, radius); + } else { + p.drawEllipse(inner); + } const auto &icon = stm->historyPollChosen; icon.paint(p, left + (radio.diameter - icon.width()) / 2, top + (radio.diameter - icon.height()) / 2, width()); } @@ -1903,12 +2037,13 @@ void Poll::paintPercent( int percentWidth, int left, int top, + int topPadding, int outerWidth, const PaintContext &context) const { const auto stm = context.messageStyle(); const auto aleft = left + st::historyPollAnswerPadding.left(); - top += st::historyPollAnswerPadding.top(); + top += topPadding; p.setFont(st::historyPollPercentFont); p.setPen(stm->historyTextFg); @@ -1923,22 +2058,23 @@ void Poll::paintFilling( float64 filling, int left, int top, + int topPadding, int width, int contentWidth, - int height, const PaintContext &context) const { - const auto bottom = top + height; const auto st = context.st; const auto stm = context.messageStyle(); const auto aleft = left + st::historyPollAnswerPadding.left(); - top += st::historyPollAnswerPadding.top(); + top += topPadding; const auto thickness = st::historyPollFillingHeight; const auto max = contentWidth - st::historyPollFillingRight; const auto size = anim::interpolate(st::historyPollFillingMin, max, filling); const auto radius = st::historyPollFillingRadius; - const auto ftop = bottom - st::historyPollFillingBottom - thickness; + const auto ftop = top + + st::historyPollPercentFont->height + + st::historyPollFillingTop; enum class Style { Incorrect, @@ -1962,8 +2098,12 @@ void Poll::paintFilling( ? st->boxTextFgGood() : stm->msgFileBg; p.setPen(Qt::NoPen); - p.setBrush(color); PainterHighQualityEnabler hq(p); + { + p.setBrush(anim::with_alpha(color->c, st::historyPollFillingBgOpacity)); + p.drawRoundedRect(barleft, ftop, max, thickness, radius, radius); + } + p.setBrush(color); if (chosen || correct) { const auto &icon = (style == Style::Incorrect) ? st->historyPollChoiceWrong() @@ -1972,7 +2112,17 @@ void Poll::paintFilling( : stm->historyPollChoiceRight; const auto cleft = aleft - st::historyPollPercentSkip - icon.width(); const auto ctop = ftop - (icon.height() - thickness) / 2; - p.drawEllipse(cleft, ctop, icon.width(), icon.height()); + if (_flags & PollData::Flag::MultiChoice) { + p.drawRoundedRect( + cleft, + ctop, + icon.width(), + icon.height(), + st::historyPollCheckboxRadius, + st::historyPollCheckboxRadius); + } else { + p.drawEllipse(cleft, ctop, icon.width(), icon.height()); + } const auto paintContent = [&](QPainter &p) { icon.paint(p, cleft, ctop, width); @@ -2256,7 +2406,9 @@ TextState Poll::textState(QPoint point, StateRequest request) const { + paintw - st::historyPollAnswerPadding.right() - media, - tshift + st::historyPollAnswerPadding.top(), + tshift + (answer.thumbnail + ? st::historyPollAnswerPadding + : st::historyPollAnswerPaddingNoMedia).top(), media, media).contains(point)) { result.link = answer.mediaHandler; @@ -2370,6 +2522,12 @@ void Poll::unloadHeavyPart() { for (auto &recent : _recentVoters) { recent.userpic = {}; } + for (auto &answer : _answers) { + for (auto &recent : answer.recentVoters) { + recent.view = {}; + } + answer.recentVotersImage = QImage(); + } } bool Poll::hasHeavyPart() const { @@ -2378,6 +2536,11 @@ bool Poll::hasHeavyPart() const { return true; } } + for (const auto &answer : _answers) { + if (!answer.recentVotersImage.isNull()) { + return true; + } + } return false; } diff --git a/Telegram/SourceFiles/history/view/media/history_view_poll.h b/Telegram/SourceFiles/history/view/media/history_view_poll.h index 9d6ae851c8..aa496e9ba5 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_poll.h +++ b/Telegram/SourceFiles/history/view/media/history_view_poll.h @@ -159,6 +159,7 @@ private: int percentWidth, int left, int top, + int topPadding, int outerWidth, const PaintContext &context) const; void paintFilling( @@ -168,9 +169,9 @@ private: float64 filling, int left, int top, + int topPadding, int width, int contentWidth, - int height, const PaintContext &context) const; void paintInlineFooter( Painter &p, @@ -265,6 +266,7 @@ private: std::unique_ptr _solutionAttach; bool _hasSelected = false; + bool _anyAnswerHasMedia = false; bool _votedFromHere = false; mutable bool _wrongAnswerAnimated = false;