Skip to content

Commit

Permalink
Formalize zero-width emote implementation (#4314)
Browse files Browse the repository at this point in the history
Co-authored-by: Rasmus Karlsson <[email protected]>
  • Loading branch information
dnsge and pajlada committed Mar 18, 2023
1 parent db97a14 commit 0acbc0d
Show file tree
Hide file tree
Showing 18 changed files with 941 additions and 140 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
## Unversioned

- Minor: Added support for FrankerFaceZ animated emotes. (#4434)
- Bugfix: Fixed an issue where animated emotes would render on top of zero-width emotes. (#4314)
- Bugfix: Fixed an issue where it was difficult to hover a zero-width emote. (#4314)
- Dev: Ignore unhandled BTTV user-events. (#4438)
- Dev: Only log debug messages when NDEBUG is not defined. (#4442)
- Dev: Cleaned up theme related code. (#4450)
Expand Down
2 changes: 2 additions & 0 deletions src/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -420,6 +420,8 @@ set(SOURCE_FILES
widgets/Scrollbar.hpp
widgets/StreamView.cpp
widgets/StreamView.hpp
widgets/TooltipEntryWidget.cpp
widgets/TooltipEntryWidget.hpp
widgets/TooltipWidget.cpp
widgets/TooltipWidget.hpp
widgets/Window.cpp
Expand Down
20 changes: 20 additions & 0 deletions src/messages/MessageBuilder.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
4 changes: 4 additions & 0 deletions src/messages/MessageBuilder.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,10 @@ class MessageBuilder
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:
Expand Down
180 changes: 180 additions & 0 deletions src/messages/MessageElement.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand Down Expand Up @@ -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)
Expand Down
41 changes: 38 additions & 3 deletions src/messages/MessageElement.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down Expand Up @@ -321,6 +319,43 @@ class EmoteElement : public MessageElement
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:
Expand Down
30 changes: 8 additions & 22 deletions src/messages/layouts/MessageLayoutContainer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
Expand Down Expand Up @@ -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(
Expand All @@ -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)
Expand Down Expand Up @@ -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())
Expand Down
Loading

0 comments on commit 0acbc0d

Please sign in to comment.