Formalize zero-width emote implementation (#4314)
Co-authored-by: Rasmus Karlsson <rasmus.karlsson@pajlada.com>
This commit is contained in:
@@ -687,6 +687,26 @@ void MessageBuilder::append(std::unique_ptr<MessageElement> element)
|
||||
this->message().elements.push_back(std::move(element));
|
||||
}
|
||||
|
||||
bool MessageBuilder::isEmpty() const
|
||||
{
|
||||
return this->message_->elements.empty();
|
||||
}
|
||||
|
||||
MessageElement &MessageBuilder::back()
|
||||
{
|
||||
assert(!this->isEmpty());
|
||||
return *this->message().elements.back();
|
||||
}
|
||||
|
||||
std::unique_ptr<MessageElement> MessageBuilder::releaseBack()
|
||||
{
|
||||
assert(!this->isEmpty());
|
||||
|
||||
auto ptr = std::move(this->message().elements.back());
|
||||
this->message().elements.pop_back();
|
||||
return ptr;
|
||||
}
|
||||
|
||||
QString MessageBuilder::matchLink(const QString &string)
|
||||
{
|
||||
LinkParser linkParser(string);
|
||||
|
||||
@@ -123,6 +123,10 @@ protected:
|
||||
virtual void addTextOrEmoji(EmotePtr emote);
|
||||
virtual void addTextOrEmoji(const QString &value);
|
||||
|
||||
bool isEmpty() const;
|
||||
MessageElement &back();
|
||||
std::unique_ptr<MessageElement> releaseBack();
|
||||
|
||||
MessageColor textColor_ = MessageColor::Text;
|
||||
|
||||
private:
|
||||
|
||||
@@ -15,6 +15,24 @@
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
namespace {
|
||||
|
||||
// Computes the bounding box for the given vector of images
|
||||
QSize getBoundingBoxSize(const std::vector<ImagePtr> &images)
|
||||
{
|
||||
int width = 0;
|
||||
int height = 0;
|
||||
for (const auto &img : images)
|
||||
{
|
||||
width = std::max(width, img->width());
|
||||
height = std::max(height, img->height());
|
||||
}
|
||||
|
||||
return QSize(width, height);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
MessageElement::MessageElement(MessageElementFlags flags)
|
||||
: flags_(flags)
|
||||
{
|
||||
@@ -216,6 +234,168 @@ MessageLayoutElement *EmoteElement::makeImageLayoutElement(
|
||||
return new ImageLayoutElement(*this, image, size);
|
||||
}
|
||||
|
||||
LayeredEmoteElement::LayeredEmoteElement(std::vector<EmotePtr> &&emotes,
|
||||
MessageElementFlags flags,
|
||||
const MessageColor &textElementColor)
|
||||
: MessageElement(flags)
|
||||
, emotes_(std::move(emotes))
|
||||
, textElementColor_(textElementColor)
|
||||
{
|
||||
this->updateTooltips();
|
||||
}
|
||||
|
||||
void LayeredEmoteElement::addEmoteLayer(const EmotePtr &emote)
|
||||
{
|
||||
this->emotes_.push_back(emote);
|
||||
this->updateTooltips();
|
||||
}
|
||||
|
||||
void LayeredEmoteElement::addToContainer(MessageLayoutContainer &container,
|
||||
MessageElementFlags flags)
|
||||
{
|
||||
if (flags.hasAny(this->getFlags()))
|
||||
{
|
||||
if (flags.has(MessageElementFlag::EmoteImages))
|
||||
{
|
||||
auto images = this->getLoadedImages(container.getScale());
|
||||
if (images.empty())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
auto emoteScale = getSettings()->emoteScale.getValue();
|
||||
float overallScale = emoteScale * container.getScale();
|
||||
|
||||
auto largestSize = getBoundingBoxSize(images) * overallScale;
|
||||
std::vector<QSize> individualSizes;
|
||||
individualSizes.reserve(this->emotes_.size());
|
||||
for (auto img : images)
|
||||
{
|
||||
individualSizes.push_back(QSize(img->width(), img->height()) *
|
||||
overallScale);
|
||||
}
|
||||
|
||||
container.addElement(this->makeImageLayoutElement(
|
||||
images, individualSizes, largestSize)
|
||||
->setLink(this->getLink()));
|
||||
}
|
||||
else
|
||||
{
|
||||
if (this->textElement_)
|
||||
{
|
||||
this->textElement_->addToContainer(container,
|
||||
MessageElementFlag::Misc);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<ImagePtr> LayeredEmoteElement::getLoadedImages(float scale)
|
||||
{
|
||||
std::vector<ImagePtr> res;
|
||||
res.reserve(this->emotes_.size());
|
||||
|
||||
for (auto emote : this->emotes_)
|
||||
{
|
||||
auto image = emote->images.getImageOrLoaded(scale);
|
||||
if (image->isEmpty())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
res.push_back(image);
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
MessageLayoutElement *LayeredEmoteElement::makeImageLayoutElement(
|
||||
const std::vector<ImagePtr> &images, const std::vector<QSize> &sizes,
|
||||
QSize largestSize)
|
||||
{
|
||||
return new LayeredImageLayoutElement(*this, images, sizes, largestSize);
|
||||
}
|
||||
|
||||
void LayeredEmoteElement::updateTooltips()
|
||||
{
|
||||
if (!this->emotes_.empty())
|
||||
{
|
||||
QString copyStr = this->getCopyString();
|
||||
this->textElement_.reset(new TextElement(
|
||||
copyStr, MessageElementFlag::Misc, this->textElementColor_));
|
||||
this->setTooltip(copyStr);
|
||||
}
|
||||
|
||||
std::vector<QString> result;
|
||||
result.reserve(this->emotes_.size());
|
||||
|
||||
for (auto &emote : this->emotes_)
|
||||
{
|
||||
result.push_back(emote->tooltip.string);
|
||||
}
|
||||
|
||||
this->emoteTooltips_ = std::move(result);
|
||||
}
|
||||
|
||||
const std::vector<QString> &LayeredEmoteElement::getEmoteTooltips() const
|
||||
{
|
||||
return this->emoteTooltips_;
|
||||
}
|
||||
|
||||
QString LayeredEmoteElement::getCleanCopyString() const
|
||||
{
|
||||
QString result;
|
||||
for (size_t i = 0; i < this->emotes_.size(); ++i)
|
||||
{
|
||||
if (i != 0)
|
||||
{
|
||||
result += " ";
|
||||
}
|
||||
result +=
|
||||
TwitchEmotes::cleanUpEmoteCode(this->emotes_[i]->getCopyString());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
QString LayeredEmoteElement::getCopyString() const
|
||||
{
|
||||
QString result;
|
||||
for (size_t i = 0; i < this->emotes_.size(); ++i)
|
||||
{
|
||||
if (i != 0)
|
||||
{
|
||||
result += " ";
|
||||
}
|
||||
result += this->emotes_[i]->getCopyString();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
const std::vector<EmotePtr> &LayeredEmoteElement::getEmotes() const
|
||||
{
|
||||
return this->emotes_;
|
||||
}
|
||||
|
||||
std::vector<EmotePtr> LayeredEmoteElement::getUniqueEmotes() const
|
||||
{
|
||||
// Functor for std::copy_if that keeps track of seen elements
|
||||
struct NotDuplicate {
|
||||
bool operator()(const EmotePtr &element)
|
||||
{
|
||||
return seen.insert(element).second;
|
||||
}
|
||||
|
||||
private:
|
||||
std::set<EmotePtr> seen;
|
||||
};
|
||||
|
||||
// Get unique emotes while maintaining relative layering order
|
||||
NotDuplicate dup;
|
||||
std::vector<EmotePtr> unique;
|
||||
std::copy_if(this->emotes_.begin(), this->emotes_.end(),
|
||||
std::back_insert_iterator(unique), dup);
|
||||
|
||||
return unique;
|
||||
}
|
||||
|
||||
// BADGE
|
||||
BadgeElement::BadgeElement(const EmotePtr &emote, MessageElementFlags flags)
|
||||
: MessageElement(flags)
|
||||
|
||||
@@ -141,9 +141,7 @@ enum class MessageElementFlag : int64_t {
|
||||
LowercaseLink = (1LL << 29),
|
||||
OriginalLink = (1LL << 30),
|
||||
|
||||
// ZeroWidthEmotes are emotes that are supposed to overlay over any pre-existing emotes
|
||||
// e.g. BTTV's SoSnowy during christmas season or 7TV's RainTime
|
||||
ZeroWidthEmote = (1LL << 31),
|
||||
// Unused: (1LL << 31)
|
||||
|
||||
// for elements of the message reply
|
||||
RepliedMessage = (1LL << 32),
|
||||
@@ -321,6 +319,43 @@ private:
|
||||
EmotePtr emote_;
|
||||
};
|
||||
|
||||
// A LayeredEmoteElement represents multiple Emotes layered on top of each other.
|
||||
// This class takes care of rendering animated and non-animated emotes in the
|
||||
// correct order and aligning them in the right way.
|
||||
class LayeredEmoteElement : public MessageElement
|
||||
{
|
||||
public:
|
||||
LayeredEmoteElement(
|
||||
std::vector<EmotePtr> &&emotes, MessageElementFlags flags,
|
||||
const MessageColor &textElementColor = MessageColor::Text);
|
||||
|
||||
void addEmoteLayer(const EmotePtr &emote);
|
||||
|
||||
void addToContainer(MessageLayoutContainer &container,
|
||||
MessageElementFlags flags) override;
|
||||
|
||||
// Returns a concatenation of each emote layer's cleaned copy string
|
||||
QString getCleanCopyString() const;
|
||||
const std::vector<EmotePtr> &getEmotes() const;
|
||||
std::vector<EmotePtr> getUniqueEmotes() const;
|
||||
const std::vector<QString> &getEmoteTooltips() const;
|
||||
|
||||
private:
|
||||
MessageLayoutElement *makeImageLayoutElement(
|
||||
const std::vector<ImagePtr> &image, const std::vector<QSize> &sizes,
|
||||
QSize largestSize);
|
||||
|
||||
QString getCopyString() const;
|
||||
void updateTooltips();
|
||||
std::vector<ImagePtr> getLoadedImages(float scale);
|
||||
|
||||
std::vector<EmotePtr> emotes_;
|
||||
std::vector<QString> emoteTooltips_;
|
||||
|
||||
std::unique_ptr<TextElement> textElement_;
|
||||
MessageColor textElementColor_;
|
||||
};
|
||||
|
||||
class BadgeElement : public MessageElement
|
||||
{
|
||||
public:
|
||||
|
||||
@@ -67,10 +67,7 @@ void MessageLayoutContainer::clear()
|
||||
|
||||
void MessageLayoutContainer::addElement(MessageLayoutElement *element)
|
||||
{
|
||||
bool isZeroWidth =
|
||||
element->getFlags().has(MessageElementFlag::ZeroWidthEmote);
|
||||
|
||||
if (!isZeroWidth && !this->fitsInLine(element->getRect().width()))
|
||||
if (!this->fitsInLine(element->getRect().width()))
|
||||
{
|
||||
this->breakLine();
|
||||
}
|
||||
@@ -175,14 +172,6 @@ void MessageLayoutContainer::_addElement(MessageLayoutElement *element,
|
||||
this->lineHeight_ = std::max(this->lineHeight_, elementLineHeight);
|
||||
|
||||
auto xOffset = 0;
|
||||
bool isZeroWidthEmote = element->getCreator().getFlags().has(
|
||||
MessageElementFlag::ZeroWidthEmote);
|
||||
|
||||
if (isZeroWidthEmote && !isRTLMode)
|
||||
{
|
||||
xOffset -= element->getRect().width() + this->spaceWidth_;
|
||||
}
|
||||
|
||||
auto yOffset = 0;
|
||||
|
||||
if (element->getCreator().getFlags().has(
|
||||
@@ -195,7 +184,7 @@ void MessageLayoutContainer::_addElement(MessageLayoutElement *element,
|
||||
|
||||
if (getSettings()->removeSpacesBetweenEmotes &&
|
||||
element->getFlags().hasAny({MessageElementFlag::EmoteImages}) &&
|
||||
!isZeroWidthEmote && shouldRemoveSpaceBetweenEmotes())
|
||||
shouldRemoveSpaceBetweenEmotes())
|
||||
{
|
||||
// Move cursor one 'space width' to the left (right in case of RTL) to combine hug the previous emote
|
||||
if (isRTLMode)
|
||||
@@ -230,16 +219,13 @@ void MessageLayoutContainer::_addElement(MessageLayoutElement *element,
|
||||
}
|
||||
|
||||
// set current x
|
||||
if (!isZeroWidthEmote)
|
||||
if (isRTLMode)
|
||||
{
|
||||
if (isRTLMode)
|
||||
{
|
||||
this->currentX_ -= element->getRect().width();
|
||||
}
|
||||
else
|
||||
{
|
||||
this->currentX_ += element->getRect().width();
|
||||
}
|
||||
this->currentX_ -= element->getRect().width();
|
||||
}
|
||||
else
|
||||
{
|
||||
this->currentX_ += element->getRect().width();
|
||||
}
|
||||
|
||||
if (element->hasTrailingSpace())
|
||||
|
||||
@@ -15,6 +15,14 @@
|
||||
namespace {
|
||||
|
||||
const QChar RTL_EMBED(0x202B);
|
||||
|
||||
void alignRectBottomCenter(QRectF &rect, const QRectF &reference)
|
||||
{
|
||||
QPointF newCenter(reference.center().x(),
|
||||
reference.bottom() - (rect.height() / 2.0));
|
||||
rect.moveCenter(newCenter);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
namespace chatterino {
|
||||
@@ -184,6 +192,133 @@ int ImageLayoutElement::getXFromIndex(int index)
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// LAYERED IMAGE
|
||||
//
|
||||
|
||||
LayeredImageLayoutElement::LayeredImageLayoutElement(
|
||||
MessageElement &creator, std::vector<ImagePtr> images,
|
||||
std::vector<QSize> sizes, QSize largestSize)
|
||||
: MessageLayoutElement(creator, largestSize)
|
||||
, images_(std::move(images))
|
||||
, sizes_(std::move(sizes))
|
||||
{
|
||||
assert(this->images_.size() == this->sizes_.size());
|
||||
this->trailingSpace = creator.hasTrailingSpace();
|
||||
}
|
||||
|
||||
void LayeredImageLayoutElement::addCopyTextToString(QString &str, uint32_t from,
|
||||
uint32_t to) const
|
||||
{
|
||||
const auto *layeredEmoteElement =
|
||||
dynamic_cast<LayeredEmoteElement *>(&this->getCreator());
|
||||
if (layeredEmoteElement)
|
||||
{
|
||||
// cleaning is taken care in call
|
||||
str += layeredEmoteElement->getCleanCopyString();
|
||||
if (this->hasTrailingSpace())
|
||||
{
|
||||
str += " ";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int LayeredImageLayoutElement::getSelectionIndexCount() const
|
||||
{
|
||||
return this->trailingSpace ? 2 : 1;
|
||||
}
|
||||
|
||||
void LayeredImageLayoutElement::paint(QPainter &painter)
|
||||
{
|
||||
auto fullRect = QRectF(this->getRect());
|
||||
|
||||
for (size_t i = 0; i < this->images_.size(); ++i)
|
||||
{
|
||||
auto &img = this->images_[i];
|
||||
if (img == nullptr)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
auto pixmap = img->pixmapOrLoad();
|
||||
if (img->animated())
|
||||
{
|
||||
// As soon as we see an animated emote layer, we can stop rendering
|
||||
// the static emotes. The paintAnimated function will render any
|
||||
// static emotes layered on top of the first seen animated emote.
|
||||
return;
|
||||
}
|
||||
|
||||
if (pixmap)
|
||||
{
|
||||
// Matching the web chat behavior, we center the emote within the overall
|
||||
// binding box. E.g. small overlay emotes like cvMask will sit in the direct
|
||||
// center of even wide emotes.
|
||||
auto &size = this->sizes_[i];
|
||||
QRectF destRect(0, 0, size.width(), size.height());
|
||||
alignRectBottomCenter(destRect, fullRect);
|
||||
|
||||
painter.drawPixmap(destRect, *pixmap, QRectF());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void LayeredImageLayoutElement::paintAnimated(QPainter &painter, int yOffset)
|
||||
{
|
||||
auto fullRect = QRectF(this->getRect());
|
||||
fullRect.moveTop(fullRect.y() + yOffset);
|
||||
bool animatedFlag = false;
|
||||
|
||||
for (size_t i = 0; i < this->images_.size(); ++i)
|
||||
{
|
||||
auto &img = this->images_[i];
|
||||
if (img == nullptr)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// If we have a static emote layered on top of an animated emote, we need
|
||||
// to render the static emote again after animating anything below it.
|
||||
if (img->animated() || animatedFlag)
|
||||
{
|
||||
if (auto pixmap = img->pixmapOrLoad())
|
||||
{
|
||||
// Matching the web chat behavior, we center the emote within the overall
|
||||
// binding box. E.g. small overlay emotes like cvMask will sit in the direct
|
||||
// center of even wide emotes.
|
||||
auto &size = this->sizes_[i];
|
||||
QRectF destRect(0, 0, size.width(), size.height());
|
||||
alignRectBottomCenter(destRect, fullRect);
|
||||
|
||||
painter.drawPixmap(destRect, *pixmap, QRectF());
|
||||
animatedFlag = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int LayeredImageLayoutElement::getMouseOverIndex(const QPoint &abs) const
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
int LayeredImageLayoutElement::getXFromIndex(int index)
|
||||
{
|
||||
if (index <= 0)
|
||||
{
|
||||
return this->getRect().left();
|
||||
}
|
||||
else if (index == 1)
|
||||
{
|
||||
// fourtf: remove space width
|
||||
return this->getRect().right();
|
||||
}
|
||||
else
|
||||
{
|
||||
return this->getRect().right();
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// IMAGE WITH BACKGROUND
|
||||
//
|
||||
|
||||
@@ -83,6 +83,26 @@ protected:
|
||||
ImagePtr image_;
|
||||
};
|
||||
|
||||
class LayeredImageLayoutElement : public MessageLayoutElement
|
||||
{
|
||||
public:
|
||||
LayeredImageLayoutElement(MessageElement &creator,
|
||||
std::vector<ImagePtr> images,
|
||||
std::vector<QSize> sizes, QSize largestSize);
|
||||
|
||||
protected:
|
||||
void addCopyTextToString(QString &str, uint32_t from = 0,
|
||||
uint32_t to = UINT32_MAX) const override;
|
||||
int getSelectionIndexCount() const override;
|
||||
void paint(QPainter &painter) override;
|
||||
void paintAnimated(QPainter &painter, int yOffset) override;
|
||||
int getMouseOverIndex(const QPoint &abs) const override;
|
||||
int getXFromIndex(int index) override;
|
||||
|
||||
std::vector<ImagePtr> images_;
|
||||
std::vector<QSize> sizes_;
|
||||
};
|
||||
|
||||
class ImageWithBackgroundLayoutElement : public ImageLayoutElement
|
||||
{
|
||||
public:
|
||||
|
||||
Reference in New Issue
Block a user