mirror of
https://github.com/AyuGram/AyuGramDesktop.git
synced 2025-04-16 14:17:12 +02:00
Implement inline reactions dropdown.
This commit is contained in:
parent
54f5b47585
commit
95e003153a
4 changed files with 278 additions and 53 deletions
|
@ -179,6 +179,7 @@ HistoryInner::HistoryInner(
|
|||
[=] { update(); }))
|
||||
, _reactionsManager(
|
||||
std::make_unique<HistoryView::Reactions::Manager>(
|
||||
this,
|
||||
[=](QRect updated) { update(updated); }))
|
||||
, _touchSelectTimer([=] { onTouchSelect(); })
|
||||
, _touchScrollTimer([=] { onTouchScrollTimer(); })
|
||||
|
|
|
@ -14,12 +14,12 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||
#include "data/data_document.h"
|
||||
#include "data/data_document_media.h"
|
||||
#include "main/main_session.h"
|
||||
#include "base/event_filter.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 kExpandDuration = crl::time(150);
|
||||
|
@ -90,10 +90,33 @@ QRect Button::geometry() const {
|
|||
return _geometry;
|
||||
}
|
||||
|
||||
int Button::scroll() const {
|
||||
return _scroll;
|
||||
}
|
||||
|
||||
bool Button::expandUp() const {
|
||||
return (_expandDirection == ExpandDirection::Up);
|
||||
}
|
||||
|
||||
bool Button::consumeWheelEvent(not_null<QWheelEvent*> e) {
|
||||
const auto scrollMax = (_expandedInnerHeight - _expandedHeight);
|
||||
if (_state != State::Inside
|
||||
|| scrollMax <= 0
|
||||
|| !_geometry.contains(e->pos())) {
|
||||
return false;
|
||||
}
|
||||
const auto delta = e->angleDelta();
|
||||
const auto horizontal = std::abs(delta.x()) > std::abs(delta.y());
|
||||
if (horizontal) {
|
||||
return false;
|
||||
}
|
||||
const auto shift = delta.y() * (expandUp() ? 1 : -1);
|
||||
_scroll = std::clamp(_scroll + shift, 0, scrollMax);
|
||||
_update(_geometry);
|
||||
e->accept();
|
||||
return true;
|
||||
}
|
||||
|
||||
void Button::applyParameters(ButtonParameters parameters) {
|
||||
applyParameters(std::move(parameters), _update);
|
||||
}
|
||||
|
@ -130,6 +153,7 @@ void Button::applyParameters(
|
|||
void Button::updateExpandDirection(const ButtonParameters ¶meters) {
|
||||
const auto maxAddedHeight = (parameters.reactionsCount - 1)
|
||||
* (st::reactionCornerSize.height() + st::reactionCornerSkip);
|
||||
_expandedInnerHeight = _collapsed.height() + maxAddedHeight;
|
||||
const auto addedHeight = std::min(
|
||||
maxAddedHeight,
|
||||
st::reactionCornerAddedHeightMax);
|
||||
|
@ -147,6 +171,9 @@ void Button::updateGeometry(Fn<void(QRect)> update) {
|
|||
const auto added = int(base::SafeRound(
|
||||
_heightAnimation.value(_finalHeight)
|
||||
)) - _collapsed.height();
|
||||
if (!added && _state != State::Inside) {
|
||||
_scroll = 0;
|
||||
}
|
||||
const auto geometry = _collapsed.marginsAdded({
|
||||
0,
|
||||
(_expandDirection == ExpandDirection::Up) ? added : 0,
|
||||
|
@ -207,7 +234,7 @@ float64 Button::ScaleForState(State state) {
|
|||
}
|
||||
|
||||
float64 Button::OpacityForScale(float64 scale) {
|
||||
return std::max(
|
||||
return std::min(
|
||||
((scale - ScaleForState(State::Hidden))
|
||||
/ (ScaleForState(State::Shown) - ScaleForState(State::Hidden))),
|
||||
1.);
|
||||
|
@ -217,19 +244,13 @@ float64 Button::currentScale() const {
|
|||
return _scaleAnimation.value(ScaleForState(_state));
|
||||
}
|
||||
|
||||
Manager::Manager(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,
|
||||
});
|
||||
}
|
||||
}))) {
|
||||
Manager::Manager(
|
||||
QWidget *wheelEventsTarget,
|
||||
Fn<void(QRect)> buttonUpdate)
|
||||
: _outer(CountOuterSize())
|
||||
, _inner(QRectF({}, st::reactionCornerSize))
|
||||
, _innerActive(QRect({}, CountMaxSizeWithMargins({})))
|
||||
, _buttonUpdate(std::move(buttonUpdate)) {
|
||||
_inner.translate(QRectF({}, _outer).center() - _inner.center());
|
||||
_innerActive.translate(
|
||||
QRect({}, _outer).center() - _innerActive.center());
|
||||
|
@ -254,6 +275,19 @@ Manager::Manager(Fn<void(QRect)> buttonUpdate)
|
|||
_shadowBuffer = QImage(
|
||||
_outer * ratio,
|
||||
QImage::Format_ARGB32_Premultiplied);
|
||||
|
||||
if (wheelEventsTarget) {
|
||||
stealWheelEvents(wheelEventsTarget);
|
||||
}
|
||||
}
|
||||
|
||||
void Manager::stealWheelEvents(not_null<QWidget*> target) {
|
||||
base::install_event_filter(target, [=](not_null<QEvent*> e) {
|
||||
return (e->type() == QEvent::Wheel
|
||||
&& consumeWheelEvent(static_cast<QWheelEvent*>(e.get())))
|
||||
? base::EventFilterResult::Cancel
|
||||
: base::EventFilterResult::Continue;
|
||||
});
|
||||
}
|
||||
|
||||
Manager::~Manager() = default;
|
||||
|
@ -280,6 +314,7 @@ void Manager::applyList(std::vector<Data::Reaction> list) {
|
|||
return;
|
||||
}
|
||||
_list = std::move(list);
|
||||
_links = std::vector<ClickHandlerPtr>(_list.size());
|
||||
if (_list.empty()) {
|
||||
_mainReactionMedia = nullptr;
|
||||
return;
|
||||
|
@ -308,6 +343,62 @@ void Manager::setMainReactionImage(QImage image) {
|
|||
ranges::fill(_validIn, false);
|
||||
ranges::fill(_validOut, false);
|
||||
ranges::fill(_validEmoji, false);
|
||||
loadOtherReactions();
|
||||
}
|
||||
|
||||
QMarginsF Manager::innerMargins() const {
|
||||
return {
|
||||
_inner.x(),
|
||||
_inner.y(),
|
||||
_outer.width() - _inner.x() - _inner.width(),
|
||||
_outer.height() - _inner.y() - _inner.height(),
|
||||
};
|
||||
}
|
||||
|
||||
QRectF Manager::buttonInner() const {
|
||||
return buttonInner(_button.get());
|
||||
}
|
||||
|
||||
QRectF Manager::buttonInner(not_null<Button*> button) const {
|
||||
return QRectF(button->geometry()).marginsRemoved(innerMargins());
|
||||
}
|
||||
|
||||
void Manager::loadOtherReactions() {
|
||||
for (const auto &reaction : _list) {
|
||||
const auto icon = reaction.staticIcon;
|
||||
if (_otherReactions.contains(icon)) {
|
||||
continue;
|
||||
}
|
||||
auto &entry = _otherReactions.emplace(icon, OtherReactionImage{
|
||||
.media = icon->createMediaView(),
|
||||
}).first->second;
|
||||
if (const auto image = entry.media->getStickerLarge()) {
|
||||
entry.image = image->original();
|
||||
entry.media = nullptr;
|
||||
} else if (!_otherReactionsLifetime) {
|
||||
icon->session().downloaderTaskFinished(
|
||||
) | rpl::start_with_next([=] {
|
||||
checkOtherReactions();
|
||||
}, _otherReactionsLifetime);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Manager::checkOtherReactions() {
|
||||
auto all = true;
|
||||
for (auto &[icon, entry] : _otherReactions) {
|
||||
if (entry.media) {
|
||||
if (const auto image = entry.media->getStickerLarge()) {
|
||||
entry.image = image->original();
|
||||
entry.media = nullptr;
|
||||
} else {
|
||||
all = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (all) {
|
||||
_otherReactionsLifetime.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
void Manager::removeStaleButtons() {
|
||||
|
@ -326,9 +417,53 @@ void Manager::paintButtons(Painter &p, const PaintContext &context) {
|
|||
}
|
||||
}
|
||||
|
||||
ClickHandlerPtr Manager::computeButtonLink(QPoint position) const {
|
||||
if (_list.empty()) {
|
||||
return nullptr;
|
||||
}
|
||||
const auto inner = buttonInner();
|
||||
const auto top = _button->expandUp()
|
||||
? (inner.y() + inner.height() - position.y())
|
||||
: (position.y() - inner.y());
|
||||
const auto scroll = _button->scroll();
|
||||
const auto shifted = top + scroll * (_button->expandUp() ? 1 : -1);
|
||||
const auto between = st::reactionCornerSkip;
|
||||
const auto oneHeight = (st::reactionCornerSize.height() + between);
|
||||
const auto index = std::clamp(
|
||||
int(base::SafeRound(shifted + between / 2.)) / oneHeight,
|
||||
0,
|
||||
int(_list.size() - 1));
|
||||
auto &result = _links[index];
|
||||
if (!result) {
|
||||
result = resolveButtonLink(_list[index]);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
ClickHandlerPtr Manager::resolveButtonLink(
|
||||
const Data::Reaction &reaction) const {
|
||||
const auto emoji = reaction.emoji;
|
||||
const auto i = _reactionsLinks.find(emoji);
|
||||
if (i != end(_reactionsLinks)) {
|
||||
return i->second;
|
||||
}
|
||||
const auto handler = crl::guard(this, [=] {
|
||||
if (_buttonContext) {
|
||||
_chosen.fire({
|
||||
.context = _buttonContext,
|
||||
.emoji = emoji,
|
||||
});
|
||||
}
|
||||
});
|
||||
return _reactionsLinks.emplace(
|
||||
emoji,
|
||||
std::make_shared<LambdaClickHandler>(handler)
|
||||
).first->second;
|
||||
}
|
||||
|
||||
TextState Manager::buttonTextState(QPoint position) const {
|
||||
if (overCurrentButton(position)) {
|
||||
auto result = TextState(nullptr, _buttonLink);
|
||||
auto result = TextState(nullptr, computeButtonLink(position));
|
||||
result.itemId = _buttonContext;
|
||||
return result;
|
||||
}
|
||||
|
@ -339,8 +474,7 @@ bool Manager::overCurrentButton(QPoint position) const {
|
|||
if (!_button) {
|
||||
return false;
|
||||
}
|
||||
const auto geometry = _button->geometry();
|
||||
return geometry.marginsRemoved(st::reactionCornerShadow).contains(position);
|
||||
return _button && buttonInner().contains(position);
|
||||
}
|
||||
|
||||
void Manager::remove(FullMsgId context) {
|
||||
|
@ -350,6 +484,10 @@ void Manager::remove(FullMsgId context) {
|
|||
}
|
||||
}
|
||||
|
||||
bool Manager::consumeWheelEvent(not_null<QWheelEvent*> e) {
|
||||
return _button && _button->consumeWheelEvent(e);
|
||||
}
|
||||
|
||||
void Manager::paintButton(
|
||||
Painter &p,
|
||||
const PaintContext &context,
|
||||
|
@ -408,45 +546,95 @@ void Manager::paintButton(
|
|||
stm.msgBg->c,
|
||||
shadow);
|
||||
if (size.height() > _outer.height()) {
|
||||
const auto factor = style::DevicePixelRatio();
|
||||
const auto part = (source.height() / factor) / 2 - 1;
|
||||
const auto fill = size.height() - 2 * part;
|
||||
const auto half = part * factor;
|
||||
const auto top = source.height() - half;
|
||||
p.drawImage(
|
||||
position,
|
||||
_cacheInOut,
|
||||
QRect(source.x(), source.y(), source.width(), half));
|
||||
p.drawImage(
|
||||
QRect(
|
||||
position + QPoint(0, part),
|
||||
QSize(source.width() / factor, fill)),
|
||||
_cacheInOut,
|
||||
QRect(
|
||||
source.x(),
|
||||
source.y() + half,
|
||||
source.width(),
|
||||
top - half));
|
||||
p.drawImage(
|
||||
position + QPoint(0, part + fill),
|
||||
_cacheInOut,
|
||||
QRect(source.x(), source.y() + top, source.width(), half));
|
||||
paintLongImage(p, geometry, _cacheInOut, source);
|
||||
} else {
|
||||
p.drawImage(position, _cacheInOut, source);
|
||||
}
|
||||
}
|
||||
|
||||
const auto mainEmojiPosition = position + (button->expandUp()
|
||||
? QPoint(0, size.height() - _outer.height())
|
||||
: QPoint());
|
||||
p.drawImage(
|
||||
mainEmojiPosition,
|
||||
_cacheParts,
|
||||
validateEmoji(frameIndex, scale));
|
||||
if (size.height() > _outer.height()) {
|
||||
p.save();
|
||||
paintAllEmoji(p, button, scale, mainEmojiPosition);
|
||||
p.restore();
|
||||
} else {
|
||||
p.drawImage(
|
||||
mainEmojiPosition,
|
||||
_cacheParts,
|
||||
validateEmoji(frameIndex, scale));
|
||||
}
|
||||
|
||||
if (opacity != 1.) {
|
||||
p.setOpacity(1.);
|
||||
}
|
||||
}
|
||||
|
||||
void Manager::paintLongImage(
|
||||
Painter &p,
|
||||
QRect geometry,
|
||||
const QImage &image,
|
||||
QRect source) {
|
||||
const auto factor = style::DevicePixelRatio();
|
||||
const auto part = (source.height() / factor) / 2 - 1;
|
||||
const auto fill = geometry.height() - 2 * part;
|
||||
const auto half = part * factor;
|
||||
const auto top = source.height() - half;
|
||||
p.drawImage(
|
||||
geometry.topLeft(),
|
||||
_cacheInOut,
|
||||
QRect(source.x(), source.y(), source.width(), half));
|
||||
p.drawImage(
|
||||
QRect(
|
||||
geometry.topLeft() + QPoint(0, part),
|
||||
QSize(source.width() / factor, fill)),
|
||||
_cacheInOut,
|
||||
QRect(
|
||||
source.x(),
|
||||
source.y() + half,
|
||||
source.width(),
|
||||
top - half));
|
||||
p.drawImage(
|
||||
geometry.topLeft() + QPoint(0, part + fill),
|
||||
_cacheInOut,
|
||||
QRect(source.x(), source.y() + top, source.width(), half));
|
||||
}
|
||||
|
||||
void Manager::paintAllEmoji(
|
||||
Painter &p,
|
||||
not_null<Button*> button,
|
||||
float64 scale,
|
||||
QPoint mainEmojiPosition) {
|
||||
const auto clip = buttonInner(button);
|
||||
p.setClipRect(clip);
|
||||
|
||||
auto hq = PainterHighQualityEnabler(p);
|
||||
const auto between = st::reactionCornerSkip;
|
||||
const auto oneHeight = st::reactionCornerSize.height() + between;
|
||||
const auto oneSize = st::reactionCornerImage * scale;
|
||||
const auto expandUp = button->expandUp();
|
||||
const auto shift = QPoint(0, oneHeight * (expandUp ? -1 : 1));
|
||||
auto emojiPosition = mainEmojiPosition
|
||||
+ QPoint(0, button->scroll() * (expandUp ? 1 : -1));
|
||||
auto index = 0;
|
||||
for (const auto &reaction : _list) {
|
||||
const auto inner = QRectF(_inner).translated(emojiPosition);
|
||||
const auto target = QRectF(
|
||||
inner.x() + (inner.width() - oneSize) / 2,
|
||||
inner.y() + (inner.height() - oneSize) / 2,
|
||||
oneSize,
|
||||
oneSize);
|
||||
if (target.intersects(clip)) {
|
||||
const auto i = _otherReactions.find(reaction.staticIcon);
|
||||
if (i != end(_otherReactions) && !i->second.image.isNull()) {
|
||||
p.drawImage(target, i->second.image);
|
||||
}
|
||||
}
|
||||
emojiPosition += shift;
|
||||
}
|
||||
}
|
||||
|
||||
void Manager::validateCacheForPattern(
|
||||
int frameIndex,
|
||||
float64 scale,
|
||||
|
@ -567,7 +755,6 @@ QRect Manager::validateFrame(
|
|||
}
|
||||
|
||||
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);
|
||||
|
@ -585,10 +772,7 @@ QRect Manager::validateFrame(
|
|||
p.scale(scale, scale);
|
||||
p.translate(-center);
|
||||
p.drawRoundedRect(inner, radius, radius);
|
||||
//p.restore();
|
||||
|
||||
//p.drawImage(position, _cacheParts, emojiSource);
|
||||
|
||||
p.restore();
|
||||
p.end();
|
||||
valid[frameIndex] = true;
|
||||
return result;
|
||||
|
|
|
@ -74,7 +74,10 @@ public:
|
|||
[[nodiscard]] bool expandUp() const;
|
||||
[[nodiscard]] bool isHidden() const;
|
||||
[[nodiscard]] QRect geometry() const;
|
||||
[[nodiscard]] int scroll() const;
|
||||
[[nodiscard]] float64 currentScale() const;
|
||||
[[nodiscard]] bool consumeWheelEvent(not_null<QWheelEvent*> e);
|
||||
|
||||
[[nodiscard]] static float64 ScaleForState(State state);
|
||||
[[nodiscard]] static float64 OpacityForScale(float64 scale);
|
||||
|
||||
|
@ -93,8 +96,10 @@ private:
|
|||
|
||||
QRect _collapsed;
|
||||
QRect _geometry;
|
||||
int _expandedInnerHeight = 0;
|
||||
int _expandedHeight = 0;
|
||||
int _finalHeight = 0;
|
||||
int _scroll = 0;
|
||||
ExpandDirection _expandDirection = ExpandDirection::Up;
|
||||
bool _outbg = false;
|
||||
|
||||
|
@ -102,7 +107,9 @@ private:
|
|||
|
||||
class Manager final : public base::has_weak_ptr {
|
||||
public:
|
||||
explicit Manager(Fn<void(QRect)> buttonUpdate);
|
||||
Manager(
|
||||
QWidget *wheelEventsTarget,
|
||||
Fn<void(QRect)> buttonUpdate);
|
||||
~Manager();
|
||||
|
||||
void applyList(std::vector<Data::Reaction> list);
|
||||
|
@ -112,6 +119,8 @@ public:
|
|||
[[nodiscard]] TextState buttonTextState(QPoint position) const;
|
||||
void remove(FullMsgId context);
|
||||
|
||||
[[nodiscard]] bool consumeWheelEvent(not_null<QWheelEvent*> e);
|
||||
|
||||
struct Chosen {
|
||||
FullMsgId context;
|
||||
QString emoji;
|
||||
|
@ -121,8 +130,14 @@ public:
|
|||
}
|
||||
|
||||
private:
|
||||
struct OtherReactionImage {
|
||||
QImage image;
|
||||
std::shared_ptr<Data::DocumentMedia> media;
|
||||
};
|
||||
static constexpr auto kFramesCount = 30;
|
||||
|
||||
void stealWheelEvents(not_null<QWidget*> target);
|
||||
|
||||
[[nodiscard]] bool overCurrentButton(QPoint position) const;
|
||||
|
||||
void removeStaleButtons();
|
||||
|
@ -136,6 +151,16 @@ private:
|
|||
not_null<Button*> button,
|
||||
int frame,
|
||||
float64 scale);
|
||||
void paintAllEmoji(
|
||||
Painter &p,
|
||||
not_null<Button*> button,
|
||||
float64 scale,
|
||||
QPoint mainEmojiPosition);
|
||||
void paintLongImage(
|
||||
Painter &p,
|
||||
QRect geometry,
|
||||
const QImage &image,
|
||||
QRect source);
|
||||
|
||||
void setMainReactionImage(QImage image);
|
||||
void applyPatternedShadow(const QColor &shadow);
|
||||
|
@ -158,8 +183,18 @@ private:
|
|||
const QRect &geometry,
|
||||
const PaintContext &context);
|
||||
|
||||
[[nodiscard]] QMarginsF innerMargins() const;
|
||||
[[nodiscard]] QRectF buttonInner() const;
|
||||
[[nodiscard]] QRectF buttonInner(not_null<Button*> button) const;
|
||||
void loadOtherReactions();
|
||||
void checkOtherReactions();
|
||||
[[nodiscard]] ClickHandlerPtr computeButtonLink(QPoint position) const;
|
||||
[[nodiscard]] ClickHandlerPtr resolveButtonLink(
|
||||
const Data::Reaction &reaction) const;
|
||||
|
||||
rpl::event_stream<Chosen> _chosen;
|
||||
std::vector<Data::Reaction> _list;
|
||||
mutable std::vector<ClickHandlerPtr> _links;
|
||||
QSize _outer;
|
||||
QRectF _inner;
|
||||
QRect _innerActive;
|
||||
|
@ -180,11 +215,16 @@ private:
|
|||
QImage _mainReactionImage;
|
||||
rpl::lifetime _mainReactionLifetime;
|
||||
|
||||
base::flat_map<
|
||||
not_null<DocumentData*>,
|
||||
OtherReactionImage> _otherReactions;
|
||||
rpl::lifetime _otherReactionsLifetime;
|
||||
|
||||
const Fn<void(QRect)> _buttonUpdate;
|
||||
std::unique_ptr<Button> _button;
|
||||
std::vector<std::unique_ptr<Button>> _buttonHiding;
|
||||
FullMsgId _buttonContext;
|
||||
ClickHandlerPtr _buttonLink;
|
||||
mutable base::flat_map<QString, ClickHandlerPtr> _reactionsLinks;
|
||||
|
||||
};
|
||||
|
||||
|
|
|
@ -971,6 +971,6 @@ reactionCornerCenter: point(-8px, -5px);
|
|||
reactionCornerImage: 24px;
|
||||
reactionCornerShadow: margins(4px, 4px, 4px, 8px);
|
||||
reactionCornerActiveAreaPadding: margins(10px, 10px, 10px, 10px);
|
||||
reactionCornerAddedHeightMax: 130px;
|
||||
reactionCornerAddedHeightMax: 120px;
|
||||
|
||||
reactionCornerSkip: 2px;
|
||||
|
|
Loading…
Add table
Reference in a new issue