Implement (sub-optimal) painting of reactions in groups.

This commit is contained in:
John Preston 2021-12-20 12:35:45 +00:00
parent 710ef43e41
commit 2a3cf8ac58
13 changed files with 1281 additions and 980 deletions

View File

@ -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

View File

@ -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"

View File

@ -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,

View File

@ -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"

View File

@ -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);
}

View File

@ -61,6 +61,7 @@ public:
[[nodiscard]] HistoryMessageEdited *displayedEditBadge();
[[nodiscard]] bool embedReactionsInBottomInfo() const;
[[nodiscard]] bool embedReactionsInBubble() const;
int marginTop() const override;
int marginBottom() const override;

View File

@ -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<char*>(to.bits());
auto fromBytes = reinterpret_cast<const char*>(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<void(QRect)> 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<Data::Reaction> &list)
: _dropdown(parent) {
_dropdown.setAutoHiding(false);
const auto content = _dropdown.setOwnedWidget(
object_ptr<Ui::RpWidget>(&_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<State>();
content->setMouseTracking(true);
content->events(
) | rpl::start_with_next([=](not_null<QEvent*> e) {
const auto type = e->type();
if (type == QEvent::MouseMove) {
const auto position = static_cast<QMouseEvent*>(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<QString> Selector::chosen() const {
return _chosen.events();
}
[[nodiscard]] rpl::lifetime &Selector::lifetime() {
return _dropdown.lifetime();
}
Manager::Manager(QWidget *selectorParent, Fn<void(QRect)> buttonUpdate)
: _outer(CountOuterSize())
, _inner(QRectF({}, st::reactionCornerSize))
, _innerActive(QRect({}, CountMaxSizeWithMargins({})))
, _buttonUpdate(std::move(buttonUpdate))
, _buttonLink(std::make_shared<LambdaClickHandler>(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<Button>(_buttonUpdate, parameters);
} else {
_button->applyParameters(parameters);
}
}
void Manager::applyList(std::vector<Data::Reaction> list) {
constexpr auto proj = &Data::Reaction::emoji;
if (ranges::equal(_list, list, ranges::equal_to{}, proj, proj)) {
return;
}
_list = std::move(list);
if (_list.size() < 2) {
hideSelectors(anim::type::normal);
}
if (_list.empty()) {
_mainReactionMedia = nullptr;
return;
}
const auto main = _list.front().staticIcon;
if (_mainReactionMedia && _mainReactionMedia->owner() == main) {
return;
}
_mainReactionMedia = main->createMediaView();
if (const auto image = _mainReactionMedia->getStickerLarge()) {
setMainReactionImage(image->original());
} else {
main->session().downloaderTaskFinished(
) | rpl::map([=] {
return _mainReactionMedia->getStickerLarge();
}) | rpl::filter_nullptr() | rpl::take(
1
) | rpl::start_with_next([=](not_null<Image*> image) {
setMainReactionImage(image->original());
}, _mainReactionLifetime);
}
}
void Manager::setMainReactionImage(QImage image) {
_mainReactionImage = std::move(image);
ranges::fill(_validIn, false);
ranges::fill(_validOut, false);
ranges::fill(_validEmoji, false);
}
void Manager::removeStaleButtons() {
_buttonHiding.erase(
ranges::remove_if(_buttonHiding, &Button::isHidden),
end(_buttonHiding));
}
void Manager::paintButtons(Painter &p, const PaintContext &context) {
removeStaleButtons();
for (const auto &button : _buttonHiding) {
paintButton(p, context, button.get());
}
if (const auto current = _button.get()) {
paintButton(p, context, current);
}
}
TextState Manager::buttonTextState(QPoint position) const {
if (overCurrentButton(position)) {
auto result = TextState(nullptr, _buttonLink);
result.itemId = _buttonContext;
return result;
}
return {};
}
bool Manager::overCurrentButton(QPoint position) const {
if (!_button) {
return false;
}
const auto geometry = _button->geometry();
return _innerActive.translated(geometry.topLeft()).contains(position);
}
void Manager::remove(FullMsgId context) {
if (_buttonContext == context) {
_buttonContext = {};
_button = nullptr;
}
if (_selectorContext == context) {
_selectorContext = {};
_selector = nullptr;
}
}
void Manager::paintButton(
Painter &p,
const PaintContext &context,
not_null<Button*> button) {
const auto geometry = button->geometry();
if (!context.clip.intersects(geometry)) {
return;
}
const auto scale = button->currentScale();
const auto scaleMin = Button::ScaleForState(ButtonState::Hidden);
const auto scaleMax = Button::ScaleForState(ButtonState::Active);
const auto progress = (scale - scaleMin) / (scaleMax - scaleMin);
const auto frame = int(base::SafeRound(progress * (kFramesCount - 1)));
const auto useScale = scaleMin
+ (frame / float64(kFramesCount - 1)) * (scaleMax - scaleMin);
paintButton(p, context, button, frame, useScale);
}
void Manager::paintButton(
Painter &p,
const PaintContext &context,
not_null<Button*> button,
int frameIndex,
float64 scale) {
const auto opacity = Button::OpacityForScale(scale);
if (opacity == 0.) {
return;
}
const auto geometry = button->geometry();
const auto position = geometry.topLeft();
const auto size = geometry.size();
const auto outbg = button->outbg();
const auto patterned = outbg
&& context.bubblesPattern
&& !context.viewport.isEmpty()
&& !context.bubblesPattern->pixmap.size().isEmpty();
const auto shadow = context.st->shadowFg()->c;
if (opacity != 1.) {
p.setOpacity(opacity);
}
if (patterned) {
p.drawImage(
position,
_cacheParts,
validateShadow(frameIndex, scale, shadow));
validateCacheForPattern(frameIndex, scale, geometry, context);
p.drawImage(geometry, _cacheForPattern);
p.drawImage(position, _cacheParts, validateEmoji(frameIndex, scale));
} else {
const auto &stm = context.st->messageStyle(outbg, false);
const auto background = stm.msgBg->c;
const auto source = validateFrame(
outbg,
frameIndex,
scale,
stm.msgBg->c,
shadow);
p.drawImage(position, _cacheInOut, source);
}
if (opacity != 1.) {
p.setOpacity(1.);
}
}
void Manager::validateCacheForPattern(
int frameIndex,
float64 scale,
const QRect &geometry,
const PaintContext &context) {
CopyImagePart(
_cacheForPattern,
_cacheParts,
validateMask(frameIndex, scale));
auto q = QPainter(&_cacheForPattern);
q.setCompositionMode(QPainter::CompositionMode_SourceIn);
Ui::PaintPatternBubblePart(
q,
context.viewport.translated(-geometry.topLeft()),
context.bubblesPattern->pixmap,
QRect(QPoint(), _outer));
}
void Manager::applyPatternedShadow(const QColor &shadow) {
if (_shadow == shadow) {
return;
}
_shadow = shadow;
ranges::fill(_validIn, false);
ranges::fill(_validOut, false);
ranges::fill(_validShadow, false);
}
QRect Manager::cacheRect(int frameIndex, int columnIndex) const {
const auto ratio = style::DevicePixelRatio();
const auto origin = QPoint(
_outer.width() * columnIndex,
_outer.height() * frameIndex);
return QRect(ratio * origin, ratio * _outer);
}
QRect Manager::validateShadow(
int frameIndex,
float64 scale,
const QColor &shadow) {
applyPatternedShadow(shadow);
const auto result = cacheRect(frameIndex, kShadowCacheIndex);
if (_validShadow[frameIndex]) {
return result;
}
_shadowBuffer.fill(Qt::transparent);
auto p = QPainter(&_shadowBuffer);
auto hq = PainterHighQualityEnabler(p);
const auto radius = _inner.height() / 2;
const auto center = _inner.center();
const auto add = style::ConvertScale(1.5);
const auto shift = style::ConvertScale(1.);
const auto extended = _inner.marginsAdded({ add, add, add, add });
p.setPen(Qt::NoPen);
p.setBrush(shadow);
p.translate(center);
p.scale(scale, scale);
p.translate(-center);
p.drawRoundedRect(extended.translated(0, shift), radius, radius);
p.end();
_shadowBuffer = Images::prepareBlur(std::move(_shadowBuffer));
auto q = QPainter(&_cacheParts);
q.setCompositionMode(QPainter::CompositionMode_Source);
q.drawImage(result.topLeft() / style::DevicePixelRatio(), _shadowBuffer);
_validShadow[frameIndex] = true;
return result;
}
QRect Manager::validateEmoji(int frameIndex, float64 scale) {
const auto result = cacheRect(frameIndex, kEmojiCacheIndex);
if (_validEmoji[frameIndex]) {
return result;
}
auto p = QPainter(&_cacheParts);
const auto ratio = style::DevicePixelRatio();
const auto position = result.topLeft() / ratio;
p.setCompositionMode(QPainter::CompositionMode_Source);
p.fillRect(QRect(position, result.size() / ratio), Qt::transparent);
if (!_mainReactionImage.isNull()) {
const auto size = st::reactionCornerImage * scale;
const auto inner = _inner.translated(position);
const auto target = QRectF(
inner.x() + (inner.width() - size) / 2,
inner.y() + (inner.height() - size) / 2,
size,
size);
auto hq = PainterHighQualityEnabler(p);
p.drawImage(target, _mainReactionImage);
}
_validEmoji[frameIndex] = true;
return result;
}
QRect Manager::validateFrame(
bool outbg,
int frameIndex,
float64 scale,
const QColor &background,
const QColor &shadow) {
applyPatternedShadow(shadow);
auto &valid = outbg ? _validOut : _validIn;
auto &color = outbg ? _backgroundOut : _backgroundIn;
if (color != background) {
color = background;
ranges::fill(valid, false);
}
const auto columnIndex = outbg ? kOutCacheIndex : kInCacheIndex;
const auto result = cacheRect(frameIndex, columnIndex);
if (valid[frameIndex]) {
return result;
}
const auto shadowSource = validateShadow(frameIndex, scale, shadow);
const auto emojiSource = validateEmoji(frameIndex, scale);
const auto position = result.topLeft() / style::DevicePixelRatio();
auto p = QPainter(&_cacheInOut);
p.setCompositionMode(QPainter::CompositionMode_Source);
p.drawImage(position, _cacheParts, shadowSource);
p.setCompositionMode(QPainter::CompositionMode_SourceOver);
auto hq = PainterHighQualityEnabler(p);
const auto inner = _inner.translated(position);
const auto radius = inner.height() / 2;
const auto center = inner.center();
p.setPen(Qt::NoPen);
p.setBrush(background);
p.save();
p.translate(center);
p.scale(scale, scale);
p.translate(-center);
p.drawRoundedRect(inner, radius, radius);
p.restore();
p.drawImage(position, _cacheParts, emojiSource);
p.end();
valid[frameIndex] = true;
return result;
}
QRect Manager::validateMask(int frameIndex, float64 scale) {
const auto result = cacheRect(frameIndex, kMaskCacheIndex);
if (_validMask[frameIndex]) {
return result;
}
auto p = QPainter(&_cacheParts);
auto hq = PainterHighQualityEnabler(p);
const auto position = result.topLeft() / style::DevicePixelRatio();
const auto inner = _inner.translated(position);
const auto radius = inner.height() / 2;
const auto center = inner.center();
p.setPen(Qt::NoPen);
p.setBrush(Qt::white);
p.save();
p.translate(center);
p.scale(scale, scale);
p.translate(-center);
p.drawRoundedRect(inner, radius, radius);
_validMask[frameIndex] = true;
return result;
}
void Manager::showSelector(Fn<QPoint(QPoint)> mapToGlobal) {
if (!_button) {
showSelector({}, {});
} else {
const auto position = _button->geometry().topLeft();
const auto geometry = _innerActive.translated(position);
showSelector(
_buttonContext,
{ mapToGlobal(geometry.topLeft()), geometry.size() });
}
}
void Manager::showSelector(FullMsgId context, QRect globalButtonArea) {
if (globalButtonArea.isEmpty()) {
context = FullMsgId();
}
const auto changed = (_selectorContext != context);
if (_selector && changed) {
_selector->toggle(false, anim::type::normal);
_selectorHiding.push_back(std::move(_selector));
}
_selectorContext = context;
if (_list.size() < 2 || !context || (!changed && !_selector)) {
return;
} else if (!_selector) {
_selector = std::make_unique<Selector>(_selectorParent, _list);
_selector->chosen(
) | rpl::start_with_next([=](QString emoji) {
_selector->toggle(false, anim::type::normal);
_selectorHiding.push_back(std::move(_selector));
_chosen.fire({ context, std::move(emoji) });
}, _selector->lifetime());
}
const auto area = QRect(
_selectorParent->mapFromGlobal(globalButtonArea.topLeft()),
globalButtonArea.size());
_selector->showAround(area);
_selector->toggle(true, anim::type::normal);
}
void Manager::hideSelectors(anim::type animated) {
if (animated == anim::type::instant) {
_selectorHiding.clear();
_selector = nullptr;
_selectorContext = {};
} else if (_selector) {
_selector->toggle(false, anim::type::normal);
_selectorHiding.push_back(std::move(_selector));
_selectorContext = {};
}
}
} // namespace HistoryView

View File

@ -0,0 +1,208 @@
/*
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/widgets/inner_dropdown.h"
namespace Ui {
struct ChatPaintContext;
} // namespace Ui
namespace Data {
struct Reaction;
class DocumentMedia;
} // namespace Data
namespace HistoryView {
using PaintContext = Ui::ChatPaintContext;
struct TextState;
} // namespace HistoryView
namespace HistoryView::Reactions {
enum class ButtonStyle {
Bubble,
};
struct ButtonParameters {
[[nodiscard]] ButtonParameters translated(QPoint delta) const {
auto result = *this;
result.center += delta;
result.pointer += delta;
return result;
}
FullMsgId context;
QPoint center;
QPoint pointer;
ButtonStyle style = ButtonStyle::Bubble;
bool inside = false;
bool active = false;
bool outbg = false;
};
enum class ButtonState {
Hidden,
Shown,
Active,
};
class Button final {
public:
Button(Fn<void(QRect)> update, ButtonParameters parameters);
~Button();
void applyParameters(ButtonParameters parameters);
using State = ButtonState;
void applyState(State state);
[[nodiscard]] bool outbg() const;
[[nodiscard]] bool isHidden() const;
[[nodiscard]] QRect geometry() const;
[[nodiscard]] float64 currentScale() const;
[[nodiscard]] static float64 ScaleForState(State state);
[[nodiscard]] static float64 OpacityForScale(float64 scale);
private:
const Fn<void(QRect)> _update;
State _state = State::Hidden;
Ui::Animations::Simple _scaleAnimation;
QRect _geometry;
ButtonStyle _style = ButtonStyle::Bubble;
bool _outbg = false;
};
class Selector final {
public:
Selector(
QWidget *parent,
const std::vector<Data::Reaction> &list);
void showAround(QRect area);
void toggle(bool shown, anim::type animated);
[[nodiscard]] rpl::producer<QString> chosen() const;
[[nodiscard]] rpl::lifetime &lifetime();
private:
struct Element {
QString emoji;
QRect geometry;
};
Ui::InnerDropdown _dropdown;
rpl::event_stream<QString> _chosen;
std::vector<Element> _elements;
bool _fromTop = true;
bool _fromLeft = true;
};
class Manager final : public base::has_weak_ptr {
public:
Manager(QWidget *selectorParent, Fn<void(QRect)> buttonUpdate);
~Manager();
void applyList(std::vector<Data::Reaction> list);
void showButton(ButtonParameters parameters);
void paintButtons(Painter &p, const PaintContext &context);
[[nodiscard]] TextState buttonTextState(QPoint position) const;
void remove(FullMsgId context);
void showSelector(Fn<QPoint(QPoint)> mapToGlobal);
void showSelector(FullMsgId context, QRect globalButtonArea);
void hideSelectors(anim::type animated);
struct Chosen {
FullMsgId context;
QString emoji;
};
[[nodiscard]] rpl::producer<Chosen> chosen() const {
return _chosen.events();
}
private:
static constexpr auto kFramesCount = 30;
[[nodiscard]] bool overCurrentButton(QPoint position) const;
void removeStaleButtons();
void paintButton(
Painter &p,
const PaintContext &context,
not_null<Button*> button);
void paintButton(
Painter &p,
const PaintContext &context,
not_null<Button*> button,
int frame,
float64 scale);
void setMainReactionImage(QImage image);
void applyPatternedShadow(const QColor &shadow);
[[nodiscard]] QRect cacheRect(int frameIndex, int columnIndex) const;
QRect validateShadow(
int frameIndex,
float64 scale,
const QColor &shadow);
QRect validateEmoji(int frameIndex, float64 scale);
QRect validateFrame(
bool outbg,
int frameIndex,
float64 scale,
const QColor &background,
const QColor &shadow);
QRect validateMask(int frameIndex, float64 scale);
void validateCacheForPattern(
int frameIndex,
float64 scale,
const QRect &geometry,
const PaintContext &context);
rpl::event_stream<Chosen> _chosen;
std::vector<Data::Reaction> _list;
QSize _outer;
QRectF _inner;
QRect _innerActive;
QImage _cacheInOut;
QImage _cacheParts;
QImage _cacheForPattern;
QImage _shadowBuffer;
std::array<bool, kFramesCount> _validIn = { { false } };
std::array<bool, kFramesCount> _validOut = { { false } };
std::array<bool, kFramesCount> _validShadow = { { false } };
std::array<bool, kFramesCount> _validEmoji = { { false } };
std::array<bool, kFramesCount> _validMask = { { false } };
QColor _backgroundIn;
QColor _backgroundOut;
QColor _shadow;
std::shared_ptr<Data::DocumentMedia> _mainReactionMedia;
QImage _mainReactionImage;
rpl::lifetime _mainReactionLifetime;
const Fn<void(QRect)> _buttonUpdate;
std::unique_ptr<Button> _button;
std::vector<std::unique_ptr<Button>> _buttonHiding;
FullMsgId _buttonContext;
ClickHandlerPtr _buttonLink;
QWidget *_selectorParent = nullptr;
std::unique_ptr<Selector> _selector;
std::vector<std::unique_ptr<Selector>> _selectorHiding;
FullMsgId _selectorContext;
};
} // namespace HistoryView

File diff suppressed because it is too large Load Diff

View File

@ -8,34 +8,37 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#pragma once
#include "history/view/history_view_object.h"
#include "ui/effects/animations.h"
#include "ui/widgets/inner_dropdown.h"
class Image;
class Painter;
namespace Ui {
class ChatStyle;
struct ChatPaintContext;
} // namespace Ui
class DocumentData;
namespace Data {
struct Reaction;
class Session;
class DocumentMedia;
} // namespace Data
namespace Ui {
struct ChatPaintContext;
} // namespace Ui
namespace HistoryView {
using PaintContext = Ui::ChatPaintContext;
enum class PointState : char;
struct TextState;
class Message;
} // namespace HistoryView
namespace HistoryView::Reactions {
struct InlineListData {
enum class Flag : uchar {
InBubble = 0x01,
OutLayout = 0x02,
};
friend inline constexpr bool is_flag_type(Flag) { return true; };
using Flags = base::flags<Flag>;
not_null<Data::Session*> owner;
base::flat_map<QString, int> reactions;
QString chosenReaction;
Flags flags = {};
};
class InlineList final : public Object {
@ -51,201 +54,46 @@ public:
void paint(
Painter &p,
const Ui::ChatStyle *st,
const PaintContext &context,
int outerWidth,
const QRect &clip) const;
private:
struct Button {
QRect geometry;
QImage image;
QString emoji;
std::shared_ptr<::Data::DocumentMedia> media;
ClickHandlerPtr link;
QString countText;
int count = 0;
int countTextWidth = 0;
};
void layout();
void layoutReactionsText();
void layoutButtons();
void setButtonCount(Button &button, int count);
void loadButtonImage(Button &button, not_null<DocumentData*> document);
void setButtonImage(Button &button, QImage large);
[[nodiscard]] Button prepareButtonWithEmoji(const QString &emoji);
void reactionsListLoaded();
void downloadTaskFinished();
[[nodiscard]] bool assetsLoaded() const;
QSize countOptimalSize() override;
Data _data;
Ui::Text::String _reactions;
std::vector<Button> _buttons;
QSize _skipBlock;
rpl::lifetime _assetsLoadLifetime;
bool _waitingForReactionsList = false;
bool _waitingForDownloadTask = false;
};
[[nodiscard]] InlineListData InlineListDataFromMessage(
not_null<Message*> message);
enum class ButtonStyle {
Bubble,
};
struct ButtonParameters {
[[nodiscard]] ButtonParameters translated(QPoint delta) const {
auto result = *this;
result.center += delta;
result.pointer += delta;
return result;
}
FullMsgId context;
QPoint center;
QPoint pointer;
ButtonStyle style = ButtonStyle::Bubble;
bool inside = false;
bool active = false;
bool outbg = false;
};
enum class ButtonState {
Hidden,
Shown,
Active,
};
class Button final {
public:
Button(Fn<void(QRect)> update, ButtonParameters parameters);
~Button();
void applyParameters(ButtonParameters parameters);
using State = ButtonState;
void applyState(State state);
[[nodiscard]] bool outbg() const;
[[nodiscard]] bool isHidden() const;
[[nodiscard]] QRect geometry() const;
[[nodiscard]] float64 currentScale() const;
[[nodiscard]] static float64 ScaleForState(State state);
[[nodiscard]] static float64 OpacityForScale(float64 scale);
private:
const Fn<void(QRect)> _update;
State _state = State::Hidden;
Ui::Animations::Simple _scaleAnimation;
QRect _geometry;
ButtonStyle _style = ButtonStyle::Bubble;
bool _outbg = false;
};
class Selector final {
public:
Selector(
QWidget *parent,
const std::vector<Data::Reaction> &list);
void showAround(QRect area);
void toggle(bool shown, anim::type animated);
[[nodiscard]] rpl::producer<QString> chosen() const;
[[nodiscard]] rpl::lifetime &lifetime();
private:
struct Element {
QString emoji;
QRect geometry;
};
Ui::InnerDropdown _dropdown;
rpl::event_stream<QString> _chosen;
std::vector<Element> _elements;
bool _fromTop = true;
bool _fromLeft = true;
};
class Manager final : public base::has_weak_ptr {
public:
Manager(QWidget *selectorParent, Fn<void(QRect)> buttonUpdate);
~Manager();
void applyList(std::vector<Data::Reaction> list);
void showButton(ButtonParameters parameters);
void paintButtons(Painter &p, const PaintContext &context);
[[nodiscard]] TextState buttonTextState(QPoint position) const;
void remove(FullMsgId context);
void showSelector(Fn<QPoint(QPoint)> mapToGlobal);
void showSelector(FullMsgId context, QRect globalButtonArea);
void hideSelectors(anim::type animated);
struct Chosen {
FullMsgId context;
QString emoji;
};
[[nodiscard]] rpl::producer<Chosen> chosen() const {
return _chosen.events();
}
private:
static constexpr auto kFramesCount = 30;
[[nodiscard]] bool overCurrentButton(QPoint position) const;
void removeStaleButtons();
void paintButton(
Painter &p,
const PaintContext &context,
not_null<Button*> button);
void paintButton(
Painter &p,
const PaintContext &context,
not_null<Button*> button,
int frame,
float64 scale);
void setMainReactionImage(QImage image);
void applyPatternedShadow(const QColor &shadow);
[[nodiscard]] QRect cacheRect(int frameIndex, int columnIndex) const;
QRect validateShadow(
int frameIndex,
float64 scale,
const QColor &shadow);
QRect validateEmoji(int frameIndex, float64 scale);
QRect validateFrame(
bool outbg,
int frameIndex,
float64 scale,
const QColor &background,
const QColor &shadow);
QRect validateMask(int frameIndex, float64 scale);
void validateCacheForPattern(
int frameIndex,
float64 scale,
const QRect &geometry,
const PaintContext &context);
rpl::event_stream<Chosen> _chosen;
std::vector<Data::Reaction> _list;
QSize _outer;
QRectF _inner;
QRect _innerActive;
QImage _cacheInOut;
QImage _cacheParts;
QImage _cacheForPattern;
QImage _shadowBuffer;
std::array<bool, kFramesCount> _validIn = { { false } };
std::array<bool, kFramesCount> _validOut = { { false } };
std::array<bool, kFramesCount> _validShadow = { { false } };
std::array<bool, kFramesCount> _validEmoji = { { false } };
std::array<bool, kFramesCount> _validMask = { { false } };
QColor _backgroundIn;
QColor _backgroundOut;
QColor _shadow;
std::shared_ptr<Data::DocumentMedia> _mainReactionMedia;
QImage _mainReactionImage;
rpl::lifetime _mainReactionLifetime;
const Fn<void(QRect)> _buttonUpdate;
std::unique_ptr<Button> _button;
std::vector<std::unique_ptr<Button>> _buttonHiding;
FullMsgId _buttonContext;
ClickHandlerPtr _buttonLink;
QWidget *_selectorParent = nullptr;
std::unique_ptr<Selector> _selector;
std::vector<std::unique_ptr<Selector>> _selectorHiding;
FullMsgId _selectorContext;
};
} // namespace HistoryView

View File

@ -609,6 +609,7 @@ bool GroupedMedia::applyGroup(const DataMediaRange &medias) {
if (_parts.empty()) {
return false;
}
refreshCaption();
Ensures(_parts.size() <= kMaxSize);
return true;
@ -628,6 +629,33 @@ bool GroupedMedia::validateGroupParts(
return (i == count);
}
void GroupedMedia::refreshCaption() {
using PartPtrOpt = std::optional<const Part*>;
const auto captionPart = [&]() -> PartPtrOpt {
if (_mode == Mode::Column) {
return std::nullopt;
}
auto result = PartPtrOpt();
for (const auto &part : _parts) {
if (!part.item->emptyText()) {
if (result) {
return std::nullopt;
} else {
result = &part;
}
}
}
return result;
}();
if (captionPart) {
const auto &part = (*captionPart);
_caption = createCaption(part->item);
_captionItem = part->item;
} else {
_captionItem = nullptr;
}
}
not_null<Media*> GroupedMedia::main() const {
Expects(!_parts.empty());
@ -662,30 +690,6 @@ HistoryMessageEdited *GroupedMedia::displayedEditBadge() const {
}
void GroupedMedia::updateNeedBubbleState() {
using PartPtrOpt = std::optional<const Part*>;
const auto captionPart = [&]() -> PartPtrOpt {
if (_mode == Mode::Column) {
return std::nullopt;
}
auto result = PartPtrOpt();
for (const auto &part : _parts) {
if (!part.item->emptyText()) {
if (result) {
return std::nullopt;
} else {
result = &part;
}
}
}
return result;
}();
if (captionPart) {
const auto &part = (*captionPart);
_caption = createCaption(part->item);
_captionItem = part->item;
} else {
_captionItem = nullptr;
}
_needBubble = computeNeedBubble();
}

View File

@ -137,6 +137,8 @@ private:
QPoint point,
StateRequest request) const;
void refreshCaption();
[[nodiscard]] RectParts cornersFromSides(RectParts sides) const;
[[nodiscard]] QMargins groupedPadding() const;

View File

@ -955,6 +955,12 @@ sendAsButton: SendAsButton {
duration: 150;
}
reactionBottomPadding: margins(5px, 2px, 7px, 2px);
reactionBottomSize: 16px;
reactionBottomSkip: 3px;
reactionBottomBetween: 4px;
reactionBottomInBubbleLeft: -3px;
reactionCornerSize: size(27px, 19px);
reactionCornerCenter: point(-6px, -5px);
reactionCornerImage: 15px;