#include "messages/layouts/MessageLayoutElement.hpp" #include "Application.hpp" #include "messages/Emote.hpp" #include "messages/Image.hpp" #include "messages/MessageElement.hpp" #include "providers/twitch/TwitchEmotes.hpp" #include "singletons/Theme.hpp" #include "util/DebugCount.hpp" #include #include #include namespace { const QChar RTL_EMBED(0x202B); } // namespace namespace chatterino { const QRect &MessageLayoutElement::getRect() const { return this->rect_; } MessageLayoutElement::MessageLayoutElement(MessageElement &creator, const QSize &size) : creator_(creator) { this->rect_.setSize(size); DebugCount::increase("message layout elements"); } MessageLayoutElement::~MessageLayoutElement() { DebugCount::decrease("message layout elements"); } MessageElement &MessageLayoutElement::getCreator() const { return this->creator_; } void MessageLayoutElement::setPosition(QPoint point) { this->rect_.moveTopLeft(point); } bool MessageLayoutElement::hasTrailingSpace() const { return this->trailingSpace; } int MessageLayoutElement::getLine() const { return this->line_; } void MessageLayoutElement::setLine(int line) { this->line_ = line; } MessageLayoutElement *MessageLayoutElement::setTrailingSpace(bool value) { this->trailingSpace = value; return this; } MessageLayoutElement *MessageLayoutElement::setLink(const Link &_link) { this->link_ = _link; return this; } MessageLayoutElement *MessageLayoutElement::setText(const QString &_text) { this->text_ = _text; return this; } const Link &MessageLayoutElement::getLink() const { return this->link_; } const QString &MessageLayoutElement::getText() const { return this->text_; } FlagsEnum MessageLayoutElement::getFlags() const { return this->creator_.getFlags(); } // // IMAGE // ImageLayoutElement::ImageLayoutElement(MessageElement &creator, ImagePtr image, const QSize &size) : MessageLayoutElement(creator, size) , image_(std::move(image)) { this->trailingSpace = creator.hasTrailingSpace(); } void ImageLayoutElement::addCopyTextToString(QString &str, uint32_t from, uint32_t to) const { const auto *emoteElement = dynamic_cast(&this->getCreator()); if (emoteElement) { str += emoteElement->getEmote()->getCopyString(); str = TwitchEmotes::cleanUpEmoteCode(str); if (this->hasTrailingSpace()) { str += " "; } } } int ImageLayoutElement::getSelectionIndexCount() const { return this->trailingSpace ? 2 : 1; } void ImageLayoutElement::paint(QPainter &painter) { if (this->image_ == nullptr) { return; } auto pixmap = this->image_->pixmapOrLoad(); if (pixmap && !this->image_->animated()) { // fourtf: make it use qreal values painter.drawPixmap(QRectF(this->getRect()), *pixmap, QRectF()); } } void ImageLayoutElement::paintAnimated(QPainter &painter, int yOffset) { if (this->image_ == nullptr) { return; } if (this->image_->animated()) { if (auto pixmap = this->image_->pixmapOrLoad()) { auto rect = this->getRect(); rect.moveTop(rect.y() + yOffset); painter.drawPixmap(QRectF(rect), *pixmap, QRectF()); } } } int ImageLayoutElement::getMouseOverIndex(const QPoint &abs) const { return 0; } int ImageLayoutElement::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 // ImageWithBackgroundLayoutElement::ImageWithBackgroundLayoutElement( MessageElement &creator, ImagePtr image, const QSize &size, QColor color) : ImageLayoutElement(creator, image, size) , color_(color) { } void ImageWithBackgroundLayoutElement::paint(QPainter &painter) { if (this->image_ == nullptr) { return; } auto pixmap = this->image_->pixmapOrLoad(); if (pixmap && !this->image_->animated()) { painter.fillRect(QRectF(this->getRect()), this->color_); // fourtf: make it use qreal values painter.drawPixmap(QRectF(this->getRect()), *pixmap, QRectF()); } } // // IMAGE WITH CIRCLE BACKGROUND // ImageWithCircleBackgroundLayoutElement::ImageWithCircleBackgroundLayoutElement( MessageElement &creator, ImagePtr image, const QSize &imageSize, QColor color, int padding) : ImageLayoutElement(creator, image, imageSize + QSize(padding, padding) * 2) , color_(color) , imageSize_(imageSize) , padding_(padding) { } void ImageWithCircleBackgroundLayoutElement::paint(QPainter &painter) { if (this->image_ == nullptr) { return; } auto pixmap = this->image_->pixmapOrLoad(); if (pixmap && !this->image_->animated()) { QRectF boxRect(this->getRect()); painter.setPen(Qt::NoPen); painter.setBrush(QBrush(this->color_, Qt::SolidPattern)); painter.drawEllipse(boxRect); QRectF imgRect; imgRect.setTopLeft(boxRect.topLeft()); imgRect.setSize(this->imageSize_); imgRect.translate(this->padding_, this->padding_); painter.drawPixmap(imgRect, *pixmap, QRectF()); } } // // TEXT // TextLayoutElement::TextLayoutElement(MessageElement &_creator, QString &_text, const QSize &_size, QColor _color, FontStyle _style, float _scale) : MessageLayoutElement(_creator, _size) , color_(_color) , style_(_style) , scale_(_scale) { this->setText(_text); } void TextLayoutElement::listenToLinkChanges() { this->managedConnections_.managedConnect( static_cast(this->getCreator()).linkChanged, [this]() { this->setLink(this->getCreator().getLink()); }); } void TextLayoutElement::addCopyTextToString(QString &str, uint32_t from, uint32_t to) const { str += this->getText().mid(from, to - from); if (this->hasTrailingSpace()) { str += " "; } } int TextLayoutElement::getSelectionIndexCount() const { return this->getText().length() + (this->trailingSpace ? 1 : 0); } void TextLayoutElement::paint(QPainter &painter) { auto app = getApp(); QString text = this->getText(); if (text.isRightToLeft() || this->reversedNeutral) { text.prepend(RTL_EMBED); } painter.setPen(this->color_); painter.setFont(app->fonts->getFont(this->style_, this->scale_)); painter.drawText( QRectF(this->getRect().x(), this->getRect().y(), 10000, 10000), text, QTextOption(Qt::AlignLeft | Qt::AlignTop)); } void TextLayoutElement::paintAnimated(QPainter &, int) { } int TextLayoutElement::getMouseOverIndex(const QPoint &abs) const { if (abs.x() < this->getRect().left()) { return 0; } auto app = getApp(); auto metrics = app->fonts->getFontMetrics(this->style_, this->scale_); auto x = this->getRect().left(); for (auto i = 0; i < this->getText().size(); i++) { auto &&text = this->getText(); auto width = metrics.horizontalAdvance(this->getText()[i]); // accept mouse to be at only 50%+ of character width to increase index if (x + (width * 0.5) > abs.x()) { if (text.size() > i + 1 && QChar::isLowSurrogate(text[i].unicode())) { i++; } return i; } x += width; } // if (this->hasTrailingSpace() && abs.x() < this->getRect().right()) // { // return this->getSelectionIndexCount() - 1; // } return this->getSelectionIndexCount() - (this->hasTrailingSpace() ? 1 : 0); } int TextLayoutElement::getXFromIndex(int index) { auto app = getApp(); QFontMetrics metrics = app->fonts->getFontMetrics(this->style_, this->scale_); if (index <= 0) { return this->getRect().left(); } else if (index < this->getText().size()) { int x = 0; for (int i = 0; i < index; i++) { x += metrics.horizontalAdvance(this->getText()[i]); } return x + this->getRect().left(); } else { return this->getRect().right(); } } // TEXT ICON TextIconLayoutElement::TextIconLayoutElement(MessageElement &creator, const QString &_line1, const QString &_line2, float _scale, const QSize &size) : MessageLayoutElement(creator, size) , scale(_scale) , line1(_line1) , line2(_line2) { } void TextIconLayoutElement::addCopyTextToString(QString &str, uint32_t from, uint32_t to) const { } int TextIconLayoutElement::getSelectionIndexCount() const { return this->trailingSpace ? 2 : 1; } void TextIconLayoutElement::paint(QPainter &painter) { auto app = getApp(); QFont font = app->fonts->getFont(FontStyle::Tiny, this->scale); painter.setPen(app->themes->messages.textColors.system); painter.setFont(font); QTextOption option; option.setAlignment(Qt::AlignHCenter); if (this->line2.isEmpty()) { QRect _rect(this->getRect()); painter.drawText(_rect, this->line1, option); } else { painter.drawText( QPoint(this->getRect().x(), this->getRect().y() + this->getRect().height() / 2), this->line1); painter.drawText(QPoint(this->getRect().x(), this->getRect().y() + this->getRect().height()), this->line2); } } void TextIconLayoutElement::paintAnimated(QPainter &painter, int yOffset) { } int TextIconLayoutElement::getMouseOverIndex(const QPoint &abs) const { return 0; } int TextIconLayoutElement::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(); } } ReplyCurveLayoutElement::ReplyCurveLayoutElement(MessageElement &creator, int width, float thickness, float radius, float neededMargin) : MessageLayoutElement(creator, QSize(width, 0)) , pen_(QColor("#888"), thickness, Qt::SolidLine, Qt::RoundCap) , radius_(radius) , neededMargin_(neededMargin) { } void ReplyCurveLayoutElement::paint(QPainter &painter) { QRectF paintRect(this->getRect()); QPainterPath path; QRectF curveRect = paintRect.marginsRemoved(QMarginsF( this->neededMargin_, this->neededMargin_, 0, this->neededMargin_)); // Make sure that our curveRect can always fit the radius curve if (curveRect.height() < this->radius_) { curveRect.setTop(curveRect.top() - (this->radius_ - curveRect.height())); } QPointF bStartPoint(curveRect.left(), curveRect.top() + this->radius_); QPointF bEndPoint(curveRect.left() + this->radius_, curveRect.top()); QPointF bControlPoint(curveRect.topLeft()); // Draw line from bottom left to curve path.moveTo(curveRect.bottomLeft()); path.lineTo(bStartPoint); // Draw curve path path.quadTo(bControlPoint, bEndPoint); // Draw line from curve to top right path.lineTo(curveRect.topRight()); // Render curve painter.setPen(this->pen_); painter.setRenderHint(QPainter::Antialiasing); painter.drawPath(path); } void ReplyCurveLayoutElement::paintAnimated(QPainter &painter, int yOffset) { } int ReplyCurveLayoutElement::getMouseOverIndex(const QPoint &abs) const { return 0; } int ReplyCurveLayoutElement::getXFromIndex(int index) { if (index <= 0) { return this->getRect().left(); } return this->getRect().right(); } void ReplyCurveLayoutElement::addCopyTextToString(QString &str, uint32_t from, uint32_t to) const { } int ReplyCurveLayoutElement::getSelectionIndexCount() const { return 1; } } // namespace chatterino