From 2a3cf8ac5805a156377acab87c68c8d20e86a6b2 Mon Sep 17 00:00:00 2001 From: John Preston Date: Mon, 20 Dec 2021 12:35:45 +0000 Subject: [PATCH] Implement (sub-optimal) painting of reactions in groups. --- Telegram/CMakeLists.txt | 2 + .../history/history_inner_widget.cpp | 2 +- .../history/view/history_view_bottom_info.h | 2 +- .../history/view/history_view_element.cpp | 2 +- .../history/view/history_view_message.cpp | 29 +- .../history/view/history_view_message.h | 1 + .../view/history_view_react_button.cpp | 733 +++++++++++++ .../history/view/history_view_react_button.h | 208 ++++ .../history/view/history_view_reactions.cpp | 986 +++++------------- .../history/view/history_view_reactions.h | 236 +---- .../view/media/history_view_media_grouped.cpp | 52 +- .../view/media/history_view_media_grouped.h | 2 + Telegram/SourceFiles/ui/chat/chat.style | 6 + 13 files changed, 1281 insertions(+), 980 deletions(-) create mode 100644 Telegram/SourceFiles/history/view/history_view_react_button.cpp create mode 100644 Telegram/SourceFiles/history/view/history_view_react_button.h diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt index 21427c7c91..57f515dcb9 100644 --- a/Telegram/CMakeLists.txt +++ b/Telegram/CMakeLists.txt @@ -654,6 +654,8 @@ PRIVATE history/view/history_view_pinned_section.h history/view/history_view_pinned_tracker.cpp history/view/history_view_pinned_tracker.h + history/view/history_view_react_button.cpp + history/view/history_view_react_button.h history/view/history_view_reactions.cpp history/view/history_view_reactions.h history/view/history_view_replies_section.cpp diff --git a/Telegram/SourceFiles/history/history_inner_widget.cpp b/Telegram/SourceFiles/history/history_inner_widget.cpp index 7554b28052..918ad6ba68 100644 --- a/Telegram/SourceFiles/history/history_inner_widget.cpp +++ b/Telegram/SourceFiles/history/history_inner_widget.cpp @@ -20,7 +20,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/view/history_view_service_message.h" #include "history/view/history_view_cursor_state.h" #include "history/view/history_view_context_menu.h" -#include "history/view/history_view_reactions.h" +#include "history/view/history_view_react_button.h" #include "history/view/history_view_emoji_interactions.h" #include "history/history_item_components.h" #include "history/history_item_text.h" diff --git a/Telegram/SourceFiles/history/view/history_view_bottom_info.h b/Telegram/SourceFiles/history/view/history_view_bottom_info.h index eabb216dff..cf21cc9892 100644 --- a/Telegram/SourceFiles/history/view/history_view_bottom_info.h +++ b/Telegram/SourceFiles/history/view/history_view_bottom_info.h @@ -25,7 +25,7 @@ struct TextState; class BottomInfo final : public Object { public: struct Data { - enum class Flag { + enum class Flag : uchar { Edited = 0x01, OutLayout = 0x02, Sending = 0x04, diff --git a/Telegram/SourceFiles/history/view/history_view_element.cpp b/Telegram/SourceFiles/history/view/history_view_element.cpp index f2543b4055..b6eff7288a 100644 --- a/Telegram/SourceFiles/history/view/history_view_element.cpp +++ b/Telegram/SourceFiles/history/view/history_view_element.cpp @@ -15,7 +15,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/view/media/history_view_media_grouped.h" #include "history/view/media/history_view_sticker.h" #include "history/view/media/history_view_large_emoji.h" -#include "history/view/history_view_reactions.h" +#include "history/view/history_view_react_button.h" #include "history/view/history_view_cursor_state.h" #include "history/history.h" #include "base/unixtime.h" diff --git a/Telegram/SourceFiles/history/view/history_view_message.cpp b/Telegram/SourceFiles/history/view/history_view_message.cpp index 160528e275..fab4fb4947 100644 --- a/Telegram/SourceFiles/history/view/history_view_message.cpp +++ b/Telegram/SourceFiles/history/view/history_view_message.cpp @@ -13,6 +13,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/history_message.h" #include "history/view/media/history_view_media.h" #include "history/view/media/history_view_web_page.h" +#include "history/view/history_view_react_button.h" #include "history/view/history_view_reactions.h" #include "history/view/history_view_group_call_bar.h" // UserpicInRow. #include "history/view/history_view_view_button.h" // ViewButton. @@ -324,8 +325,7 @@ QSize Message::performCountOptimalSize() { refreshRightBadge(); refreshInfoSkipBlock(); - const auto displayInfo = needInfoDisplay(); - const auto reactionsInBubble = _reactions && displayInfo; + const auto reactionsInBubble = _reactions && embedReactionsInBubble(); if (_reactions) { _reactions->initDimensions(); } @@ -539,7 +539,7 @@ void Message::draw(Painter &p, const PaintContext &context) const { auto mediaOnTop = (mediaDisplayed && media->isBubbleTop()) || (entry && entry->isBubbleTop()); const auto displayInfo = needInfoDisplay(); - const auto reactionsInBubble = _reactions && displayInfo; + const auto reactionsInBubble = _reactions && embedReactionsInBubble(); auto mediaSelectionIntervals = (!context.selected() && mediaDisplayed) ? media->getBubbleSelectionIntervals(context.selection) @@ -601,7 +601,7 @@ void Message::draw(Painter &p, const PaintContext &context) const { g.setHeight(g.height() - reactionsHeight); const auto reactionsPosition = QPoint(g.left(), g.top() + g.height() + st::mediaInBubbleSkip); p.translate(reactionsPosition); - _reactions->paint(p, context.st, g.width(), context.clip.translated(-reactionsPosition)); + _reactions->paint(p, context, g.width(), context.clip.translated(-reactionsPosition)); p.translate(-reactionsPosition); } @@ -666,7 +666,7 @@ void Message::draw(Painter &p, const PaintContext &context) const { trect.setHeight(trect.height() - reactionsHeight); const auto reactionsPosition = QPoint(trect.left(), trect.top() + trect.height() + st::mediaInBubbleSkip); p.translate(reactionsPosition); - _reactions->paint(p, context.st, g.width(), context.clip.translated(-reactionsPosition)); + _reactions->paint(p, context, g.width(), context.clip.translated(-reactionsPosition)); p.translate(-reactionsPosition); } @@ -1073,7 +1073,7 @@ PointState Message::pointState(QPoint point) const { const auto media = this->media(); const auto item = message(); - const auto reactionsInBubble = _reactions && needInfoDisplay(); + const auto reactionsInBubble = _reactions && embedReactionsInBubble(); if (drawBubble()) { if (!g.contains(point)) { return PointState::Outside; @@ -1247,7 +1247,7 @@ TextState Message::textState( return result; } - const auto reactionsInBubble = _reactions && needInfoDisplay(); + const auto reactionsInBubble = _reactions && embedReactionsInBubble(); auto keyboard = item->inlineReplyKeyboard(); auto keyboardHeight = 0; if (keyboard) { @@ -1912,6 +1912,10 @@ bool Message::embedReactionsInBottomInfo() const { return data()->history()->peer->isUser(); } +bool Message::embedReactionsInBubble() const { + return needInfoDisplay(); +} + void Message::refreshReactions() { const auto item = data(); const auto &list = item->reactions(); @@ -2412,7 +2416,10 @@ void Message::updateMediaInBubbleState() { const auto item = message(); const auto media = this->media(); - const auto reactionsInBubble = (_reactions && needInfoDisplay()); + if (media) { + media->updateNeedBubbleState(); + } + const auto reactionsInBubble = (_reactions && embedReactionsInBubble()); auto mediaHasSomethingBelow = (_viewButton != nullptr) || reactionsInBubble; auto mediaHasSomethingAbove = false; @@ -2437,7 +2444,6 @@ void Message::updateMediaInBubbleState() { return; } - media->updateNeedBubbleState(); if (!drawBubble()) { media->setInBubbleState(MediaInBubbleState::None); return; @@ -2600,8 +2606,7 @@ int Message::resizeContentGetHeight(int newWidth) { } } const auto textWidth = qMax(contentWidth - st::msgPadding.left() - st::msgPadding.right(), 1); - const auto displayInfo = needInfoDisplay(); - const auto reactionsInBubble = _reactions && displayInfo; + const auto reactionsInBubble = _reactions && embedReactionsInBubble(); const auto bottomInfoHeight = _bottomInfo.resizeGetHeight( std::min( _bottomInfo.optimalSize().width(), @@ -2682,7 +2687,7 @@ int Message::resizeContentGetHeight(int newWidth) { reply->resize(contentWidth - st::msgPadding.left() - st::msgPadding.right()); newHeight += st::msgReplyPadding.top() + st::msgReplyBarSize.height() + st::msgReplyPadding.bottom(); } - if (displayInfo) { + if (needInfoDisplay()) { newHeight += (bottomInfoHeight - st::msgDateFont->height); } diff --git a/Telegram/SourceFiles/history/view/history_view_message.h b/Telegram/SourceFiles/history/view/history_view_message.h index aeaca7fee3..80b72bf536 100644 --- a/Telegram/SourceFiles/history/view/history_view_message.h +++ b/Telegram/SourceFiles/history/view/history_view_message.h @@ -61,6 +61,7 @@ public: [[nodiscard]] HistoryMessageEdited *displayedEditBadge(); [[nodiscard]] bool embedReactionsInBottomInfo() const; + [[nodiscard]] bool embedReactionsInBubble() const; int marginTop() const override; int marginBottom() const override; diff --git a/Telegram/SourceFiles/history/view/history_view_react_button.cpp b/Telegram/SourceFiles/history/view/history_view_react_button.cpp new file mode 100644 index 0000000000..de11639026 --- /dev/null +++ b/Telegram/SourceFiles/history/view/history_view_react_button.cpp @@ -0,0 +1,733 @@ +/* +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 "history/view/history_view_react_button.h" + +#include "history/view/history_view_cursor_state.h" +#include "ui/chat/chat_style.h" +#include "ui/chat/message_bubble.h" +#include "data/data_message_reactions.h" +#include "data/data_document.h" +#include "data/data_document_media.h" +#include "main/main_session.h" +#include "styles/style_chat.h" + +namespace HistoryView::Reactions { +namespace { + +constexpr auto kItemsPerRow = 5; +constexpr auto kToggleDuration = crl::time(80); +constexpr auto kActivateDuration = crl::time(150); +constexpr auto kInCacheIndex = 0; +constexpr auto kOutCacheIndex = 1; +constexpr auto kShadowCacheIndex = 0; +constexpr auto kEmojiCacheIndex = 1; +constexpr auto kMaskCacheIndex = 2; +constexpr auto kCacheColumsCount = 3; + +[[nodiscard]] QSize CountMaxSizeWithMargins(style::margins margins) { + const auto extended = QRect( + QPoint(), + st::reactionCornerSize + ).marginsAdded(margins); + const auto scale = Button::ScaleForState(ButtonState::Active); + return QSize( + int(base::SafeRound(extended.width() * scale)), + int(base::SafeRound(extended.height() * scale))); +} + +[[nodiscard]] QSize CountOuterSize() { + return CountMaxSizeWithMargins(st::reactionCornerShadow); +} + +void CopyImagePart(QImage &to, const QImage &from, QRect source) { + Expects(to.size() == source.size()); + Expects(QRect(QPoint(), from.size()).contains(source)); + Expects(to.format() == from.format()); + Expects(to.bytesPerLine() == to.width() * 4); + + const auto perPixel = 4; + const auto fromPerLine = from.bytesPerLine(); + const auto toPerLine = to.bytesPerLine(); + auto toBytes = reinterpret_cast(to.bits()); + auto fromBytes = reinterpret_cast(from.bits()) + + (source.y() * fromPerLine) + + (source.x() * perPixel); + for (auto y = 0, height = source.height(); y != height; ++y) { + memcpy(toBytes, fromBytes, toPerLine); + toBytes += toPerLine; + fromBytes += fromPerLine; + } +} + +} // namespace + +Button::Button( + Fn update, + ButtonParameters parameters) +: _update(std::move(update)) { + const auto initial = QRect(QPoint(), CountOuterSize()); + _geometry = initial.translated(parameters.center - initial.center()); + _outbg = parameters.outbg; + applyState(parameters.active ? State::Active : State::Shown); +} + +Button::~Button() = default; + +bool Button::outbg() const { + return _outbg; +} + +bool Button::isHidden() const { + return (_state == State::Hidden) && !_scaleAnimation.animating(); +} + +QRect Button::geometry() const { + return _geometry; +} + +void Button::applyParameters(ButtonParameters parameters) { + const auto geometry = _geometry.translated( + parameters.center - _geometry.center()); + if (_outbg != parameters.outbg) { + _outbg = parameters.outbg; + _update(_geometry); + } + if (_geometry != geometry) { + if (!_geometry.isNull()) { + _update(_geometry); + } + _geometry = geometry; + _update(_geometry); + } + applyState(parameters.active ? State::Active : State::Shown); +} + +void Button::applyState(State state) { + if (_state == state) { + return; + } + const auto duration = (state == State::Hidden + || _state == State::Hidden) + ? kToggleDuration + : kActivateDuration; + _scaleAnimation.start( + [=] { _update(_geometry); }, + ScaleForState(_state), + ScaleForState(state), + duration); + _state = state; +} + +float64 Button::ScaleForState(State state) { + switch (state) { + case State::Hidden: return 0.7; + case State::Shown: return 1.; + case State::Active: return 1.4; + } + Unexpected("State in ReactionButton::ScaleForState."); +} + +float64 Button::OpacityForScale(float64 scale) { + return (scale >= 1.) + ? 1. + : ((scale - ScaleForState(State::Hidden)) + / (ScaleForState(State::Shown) - ScaleForState(State::Hidden))); +} + +float64 Button::currentScale() const { + return _scaleAnimation.value(ScaleForState(_state)); +} + +Selector::Selector( + QWidget *parent, + const std::vector &list) +: _dropdown(parent) { + _dropdown.setAutoHiding(false); + + const auto content = _dropdown.setOwnedWidget( + object_ptr(&_dropdown)); + + const auto count = int(list.size()); + const auto single = st::reactionPopupImage; + const auto padding = st::reactionPopupPadding; + const auto width = padding.left() + single + padding.right(); + const auto height = padding.top() + single + padding.bottom(); + const auto rows = (count + kItemsPerRow - 1) / kItemsPerRow; + const auto columns = (int(list.size()) + rows - 1) / rows; + const auto inner = QRect(0, 0, columns * width, rows * height); + const auto outer = inner.marginsAdded(padding); + content->resize(outer.size()); + + _elements.reserve(list.size()); + auto x = padding.left(); + auto y = padding.top(); + auto row = -1; + auto perrow = 0; + while (_elements.size() != list.size()) { + if (!perrow) { + ++row; + perrow = (list.size() - _elements.size()) / (rows - row); + x = (outer.width() - perrow * width) / 2; + } + auto &reaction = list[_elements.size()]; + _elements.push_back({ + .emoji = reaction.emoji, + .geometry = QRect(x, y + row * height, width, height), + }); + x += width; + --perrow; + } + + struct State { + int selected = -1; + int pressed = -1; + }; + const auto state = content->lifetime().make_state(); + content->setMouseTracking(true); + content->events( + ) | rpl::start_with_next([=](not_null e) { + const auto type = e->type(); + if (type == QEvent::MouseMove) { + const auto position = static_cast(e.get())->pos(); + const auto i = ranges::find_if(_elements, [&](const Element &e) { + return e.geometry.contains(position); + }); + const auto selected = (i != end(_elements)) + ? int(i - begin(_elements)) + : -1; + if (state->selected != selected) { + state->selected = selected; + content->update(); + } + } else if (type == QEvent::MouseButtonPress) { + state->pressed = state->selected; + content->update(); + } else if (type == QEvent::MouseButtonRelease) { + const auto pressed = std::exchange(state->pressed, -1); + if (pressed >= 0) { + content->update(); + if (pressed == state->selected) { + _chosen.fire_copy(_elements[pressed].emoji); + } + } + } + }, content->lifetime()); + + content->paintRequest( + ) | rpl::start_with_next([=] { + auto p = QPainter(content); + const auto radius = st::roundRadiusSmall; + { + auto hq = PainterHighQualityEnabler(p); + p.setBrush(st::emojiPanBg); + p.setPen(Qt::NoPen); + p.drawRoundedRect(content->rect(), radius, radius); + } + auto index = 0; + const auto activeIndex = (state->pressed >= 0) + ? state->pressed + : state->selected; + const auto size = Ui::Emoji::GetSizeNormal(); + for (const auto &element : _elements) { + const auto active = (index++ == activeIndex); + if (active) { + auto hq = PainterHighQualityEnabler(p); + p.setBrush(st::windowBgOver); + p.setPen(Qt::NoPen); + p.drawRoundedRect(element.geometry, radius, radius); + } + if (const auto emoji = Ui::Emoji::Find(element.emoji)) { + Ui::Emoji::Draw( + p, + emoji, + size, + element.geometry.x() + (width - size) / 2, + element.geometry.y() + (height - size) / 2); + } + } + }, content->lifetime()); + + _dropdown.resizeToContent(); +} + +void Selector::showAround(QRect area) { + const auto parent = _dropdown.parentWidget(); + const auto left = std::min( + std::max(area.x() + (area.width() - _dropdown.width()) / 2, 0), + parent->width() - _dropdown.width()); + _fromTop = (area.y() >= _dropdown.height()); + _fromLeft = (area.center().x() - left + <= left + _dropdown.width() - area.center().x()); + const auto top = _fromTop + ? (area.y() - _dropdown.height()) + : (area.y() + area.height()); + _dropdown.move(left, top); +} + +void Selector::toggle(bool shown, anim::type animated) { + if (animated == anim::type::normal) { + if (shown) { + using Origin = Ui::PanelAnimation::Origin; + _dropdown.showAnimated(_fromTop + ? (_fromLeft ? Origin::BottomLeft : Origin::BottomRight) + : (_fromLeft ? Origin::TopLeft : Origin::TopRight)); + } else { + _dropdown.hideAnimated(); + } + } else if (shown) { + _dropdown.showFast(); + } else { + _dropdown.hideFast(); + } +} + +[[nodiscard]] rpl::producer Selector::chosen() const { + return _chosen.events(); +} + +[[nodiscard]] rpl::lifetime &Selector::lifetime() { + return _dropdown.lifetime(); +} + +Manager::Manager(QWidget *selectorParent, Fn buttonUpdate) +: _outer(CountOuterSize()) +, _inner(QRectF({}, st::reactionCornerSize)) +, _innerActive(QRect({}, CountMaxSizeWithMargins({}))) +, _buttonUpdate(std::move(buttonUpdate)) +, _buttonLink(std::make_shared(crl::guard(this, [=] { + if (_buttonContext && !_list.empty()) { + _chosen.fire({ + .context = _buttonContext, + .emoji = _list.front().emoji, + }); + } +}))) +, _selectorParent(selectorParent) { + _inner.translate(QRectF({}, _outer).center() - _inner.center()); + _innerActive.translate( + QRect({}, _outer).center() - _innerActive.center()); + + const auto ratio = style::DevicePixelRatio(); + _cacheInOut = QImage( + _outer.width() * 2 * ratio, + _outer.height() * kFramesCount * ratio, + QImage::Format_ARGB32_Premultiplied); + _cacheInOut.setDevicePixelRatio(ratio); + _cacheInOut.fill(Qt::transparent); + _cacheParts = QImage( + _outer.width() * kCacheColumsCount * ratio, + _outer.height() * kFramesCount * ratio, + QImage::Format_ARGB32_Premultiplied); + _cacheParts.setDevicePixelRatio(ratio); + _cacheParts.fill(Qt::transparent); + _cacheForPattern = QImage( + _outer * ratio, + QImage::Format_ARGB32_Premultiplied); + _cacheForPattern.setDevicePixelRatio(ratio); + _shadowBuffer = QImage( + _outer * ratio, + QImage::Format_ARGB32_Premultiplied); +} + +Manager::~Manager() = default; + +void Manager::showButton(ButtonParameters parameters) { + if (_button && _buttonContext != parameters.context) { + if (!parameters.context + && _selector + && _selectorContext == _buttonContext) { + return; + } + _button->applyState(ButtonState::Hidden); + _buttonHiding.push_back(std::move(_button)); + } + _buttonContext = parameters.context; + if (!_buttonContext || _list.size() < 2) { + hideSelectors(anim::type::normal); + return; + } + if (!_button) { + _button = std::make_unique