mirror of
https://github.com/AyuGram/AyuGramDesktop.git
synced 2025-04-16 14:17:12 +02:00
Implement (sub-optimal) painting of reactions in groups.
This commit is contained in:
parent
710ef43e41
commit
2a3cf8ac58
13 changed files with 1281 additions and 980 deletions
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -61,6 +61,7 @@ public:
|
|||
[[nodiscard]] HistoryMessageEdited *displayedEditBadge();
|
||||
|
||||
[[nodiscard]] bool embedReactionsInBottomInfo() const;
|
||||
[[nodiscard]] bool embedReactionsInBubble() const;
|
||||
|
||||
int marginTop() const override;
|
||||
int marginBottom() const override;
|
||||
|
|
733
Telegram/SourceFiles/history/view/history_view_react_button.cpp
Normal file
733
Telegram/SourceFiles/history/view/history_view_react_button.cpp
Normal 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
|
208
Telegram/SourceFiles/history/view/history_view_react_button.h
Normal file
208
Telegram/SourceFiles/history/view/history_view_react_button.h
Normal 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
|
@ -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
|
||||
|
|
|
@ -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 = ∂
|
||||
}
|
||||
}
|
||||
}
|
||||
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 = ∂
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}();
|
||||
if (captionPart) {
|
||||
const auto &part = (*captionPart);
|
||||
_caption = createCaption(part->item);
|
||||
_captionItem = part->item;
|
||||
} else {
|
||||
_captionItem = nullptr;
|
||||
}
|
||||
_needBubble = computeNeedBubble();
|
||||
}
|
||||
|
||||
|
|
|
@ -137,6 +137,8 @@ private:
|
|||
QPoint point,
|
||||
StateRequest request) const;
|
||||
|
||||
void refreshCaption();
|
||||
|
||||
[[nodiscard]] RectParts cornersFromSides(RectParts sides) const;
|
||||
[[nodiscard]] QMargins groupedPadding() const;
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Add table
Reference in a new issue