mirror of
https://github.com/Chatterino/chatterino2.git
synced 2024-11-21 22:24:07 +01:00
chore: cleanup, document, and test some RTL code (#5473)
This commit is contained in:
parent
a2cbe6377d
commit
ff7cc09f8b
8 changed files with 413 additions and 104 deletions
|
@ -55,6 +55,7 @@
|
||||||
- Dev: Deprecate Qt 5.12. (#5396)
|
- Dev: Deprecate Qt 5.12. (#5396)
|
||||||
- Dev: The running Qt version is now shown in the about page if it differs from the compiled version. (#5501)
|
- Dev: The running Qt version is now shown in the about page if it differs from the compiled version. (#5501)
|
||||||
- Dev: `FlagsEnum` is now `constexpr`. (#5510)
|
- Dev: `FlagsEnum` is now `constexpr`. (#5510)
|
||||||
|
- Dev: Documented and added tests to RTL handling. (#5473)
|
||||||
|
|
||||||
## 2.5.1
|
## 2.5.1
|
||||||
|
|
||||||
|
|
|
@ -53,24 +53,27 @@ Frames::Frames(QList<Frame> &&frames)
|
||||||
getApp()->getEmotes()->getGIFTimer().signal.connect([this] {
|
getApp()->getEmotes()->getGIFTimer().signal.connect([this] {
|
||||||
this->advance();
|
this->advance();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
auto totalLength =
|
||||||
|
std::accumulate(this->items_.begin(), this->items_.end(), 0UL,
|
||||||
|
[](auto init, auto &&frame) {
|
||||||
|
return init + frame.duration;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (totalLength == 0)
|
||||||
|
{
|
||||||
|
this->durationOffset_ = 0;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
this->durationOffset_ = std::min<int>(
|
||||||
|
int(getApp()->getEmotes()->getGIFTimer().position() %
|
||||||
|
totalLength),
|
||||||
|
60000);
|
||||||
|
}
|
||||||
|
this->processOffset();
|
||||||
}
|
}
|
||||||
|
|
||||||
auto totalLength = std::accumulate(this->items_.begin(), this->items_.end(),
|
|
||||||
0UL, [](auto init, auto &&frame) {
|
|
||||||
return init + frame.duration;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (totalLength == 0)
|
|
||||||
{
|
|
||||||
this->durationOffset_ = 0;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
this->durationOffset_ = std::min<int>(
|
|
||||||
int(getApp()->getEmotes()->getGIFTimer().position() % totalLength),
|
|
||||||
60000);
|
|
||||||
}
|
|
||||||
this->processOffset();
|
|
||||||
DebugCount::increase("image bytes", this->memoryUsage());
|
DebugCount::increase("image bytes", this->memoryUsage());
|
||||||
DebugCount::increase("image bytes (ever loaded)", this->memoryUsage());
|
DebugCount::increase("image bytes (ever loaded)", this->memoryUsage());
|
||||||
}
|
}
|
||||||
|
|
|
@ -593,8 +593,6 @@ void SingleLineTextElement::addToContainer(MessageLayoutContainer &container,
|
||||||
// once we encounter an emote or reach the end of the message text. */
|
// once we encounter an emote or reach the end of the message text. */
|
||||||
QString currentText;
|
QString currentText;
|
||||||
|
|
||||||
container.first = FirstWord::Neutral;
|
|
||||||
|
|
||||||
bool firstIteration = true;
|
bool firstIteration = true;
|
||||||
for (Word &word : this->words_)
|
for (Word &word : this->words_)
|
||||||
{
|
{
|
||||||
|
|
|
@ -119,9 +119,9 @@ private:
|
||||||
QPixmap *ensureBuffer(QPainter &painter, int width);
|
QPixmap *ensureBuffer(QPainter &painter, int width);
|
||||||
|
|
||||||
// variables
|
// variables
|
||||||
MessagePtr message_;
|
const MessagePtr message_;
|
||||||
MessageLayoutContainer container_;
|
MessageLayoutContainer container_;
|
||||||
std::unique_ptr<QPixmap> buffer_{};
|
std::unique_ptr<QPixmap> buffer_;
|
||||||
bool bufferValid_ = false;
|
bool bufferValid_ = false;
|
||||||
|
|
||||||
int height_ = 0;
|
int height_ = 0;
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
#include "MessageLayoutContainer.hpp"
|
#include "messages/layouts/MessageLayoutContainer.hpp"
|
||||||
|
|
||||||
#include "Application.hpp"
|
#include "Application.hpp"
|
||||||
#include "messages/layouts/MessageLayoutContext.hpp"
|
#include "messages/layouts/MessageLayoutContext.hpp"
|
||||||
|
@ -14,6 +14,7 @@
|
||||||
#include <QDebug>
|
#include <QDebug>
|
||||||
#include <QMargins>
|
#include <QMargins>
|
||||||
#include <QPainter>
|
#include <QPainter>
|
||||||
|
#include <QVarLengthArray>
|
||||||
|
|
||||||
#include <optional>
|
#include <optional>
|
||||||
|
|
||||||
|
@ -55,7 +56,6 @@ void MessageLayoutContainer::beginLayout(int width, float scale,
|
||||||
this->currentWordId_ = 0;
|
this->currentWordId_ = 0;
|
||||||
this->canAddMessages_ = true;
|
this->canAddMessages_ = true;
|
||||||
this->isCollapsed_ = false;
|
this->isCollapsed_ = false;
|
||||||
this->wasPrevReversed_ = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void MessageLayoutContainer::endLayout()
|
void MessageLayoutContainer::endLayout()
|
||||||
|
@ -71,7 +71,7 @@ void MessageLayoutContainer::endLayout()
|
||||||
QSize(this->dotdotdotWidth_, this->textLineHeight_),
|
QSize(this->dotdotdotWidth_, this->textLineHeight_),
|
||||||
QColor("#00D80A"), FontStyle::ChatMediumBold, this->scale_);
|
QColor("#00D80A"), FontStyle::ChatMediumBold, this->scale_);
|
||||||
|
|
||||||
if (this->first == FirstWord::RTL)
|
if (this->isRTL())
|
||||||
{
|
{
|
||||||
// Shift all elements in the next line to the left
|
// Shift all elements in the next line to the left
|
||||||
for (auto i = this->lines_.back().startIndex;
|
for (auto i = this->lines_.back().startIndex;
|
||||||
|
@ -125,9 +125,9 @@ void MessageLayoutContainer::addElementNoLineBreak(
|
||||||
|
|
||||||
void MessageLayoutContainer::breakLine()
|
void MessageLayoutContainer::breakLine()
|
||||||
{
|
{
|
||||||
if (this->containsRTL)
|
if (this->lineContainsRTL_ || this->isRTL())
|
||||||
{
|
{
|
||||||
for (int i = 0; i < this->elements_.size(); i++)
|
for (size_t i = 0; i < this->elements_.size(); i++)
|
||||||
{
|
{
|
||||||
if (this->elements_[i]->getFlags().has(
|
if (this->elements_[i]->getFlags().has(
|
||||||
MessageElementFlag::Username))
|
MessageElementFlag::Username))
|
||||||
|
@ -136,6 +136,7 @@ void MessageLayoutContainer::breakLine()
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
this->lineContainsRTL_ = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
int xOffset = 0;
|
int xOffset = 0;
|
||||||
|
@ -404,7 +405,7 @@ size_t MessageLayoutContainer::getSelectionIndex(QPoint point) const
|
||||||
|
|
||||||
size_t index = 0;
|
size_t index = 0;
|
||||||
|
|
||||||
for (auto i = 0; i < lineEnd; i++)
|
for (size_t i = 0; i < lineEnd; i++)
|
||||||
{
|
{
|
||||||
auto &&element = this->elements_[i];
|
auto &&element = this->elements_[i];
|
||||||
|
|
||||||
|
@ -565,30 +566,37 @@ int MessageLayoutContainer::nextWordId()
|
||||||
|
|
||||||
void MessageLayoutContainer::addElement(MessageLayoutElement *element,
|
void MessageLayoutContainer::addElement(MessageLayoutElement *element,
|
||||||
const bool forceAdd,
|
const bool forceAdd,
|
||||||
const int prevIndex)
|
const qsizetype prevIndex)
|
||||||
{
|
{
|
||||||
if (!this->canAddElements() && !forceAdd)
|
if (!this->canAddElements() && !forceAdd)
|
||||||
{
|
{
|
||||||
|
assert(prevIndex == -2 &&
|
||||||
|
"element is still referenced in this->elements_");
|
||||||
delete element;
|
delete element;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool isRTLMode = this->first == FirstWord::RTL && prevIndex != -2;
|
|
||||||
bool isAddingMode = prevIndex == -2;
|
bool isAddingMode = prevIndex == -2;
|
||||||
|
bool isRTLAdjusting = this->isRTL() && !isAddingMode;
|
||||||
|
|
||||||
// This lambda contains the logic for when to step one 'space width' back for compact x emotes
|
/// Returns `true` if a previously added `spaceWidth_` should be removed
|
||||||
auto shouldRemoveSpaceBetweenEmotes = [this, prevIndex]() -> bool {
|
/// before the to be added emote. The space was inserted by the
|
||||||
|
/// previous element but isn't desired as "removeSpacesBetweenEmotes" is
|
||||||
|
/// enabled and both elements are emotes.
|
||||||
|
auto shouldRemoveSpaceBetweenEmotes = [this, prevIndex,
|
||||||
|
isAddingMode]() -> bool {
|
||||||
if (prevIndex == -1 || this->elements_.empty())
|
if (prevIndex == -1 || this->elements_.empty())
|
||||||
{
|
{
|
||||||
// No previous element found
|
// No previous element found
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const auto &lastElement = prevIndex == -2 ? this->elements_.back()
|
const auto &lastElement =
|
||||||
: this->elements_[prevIndex];
|
isAddingMode ? this->elements_.back() : this->elements_[prevIndex];
|
||||||
|
|
||||||
if (!lastElement)
|
if (!lastElement)
|
||||||
{
|
{
|
||||||
|
assert(false && "Empty element in container found");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -608,23 +616,24 @@ void MessageLayoutContainer::addElement(MessageLayoutElement *element,
|
||||||
return lastElement->getFlags().has(MessageElementFlag::EmoteImages);
|
return lastElement->getFlags().has(MessageElementFlag::EmoteImages);
|
||||||
};
|
};
|
||||||
|
|
||||||
if (element->getText().isRightToLeft())
|
bool isRTLElement = element->getText().isRightToLeft();
|
||||||
|
if (isRTLElement)
|
||||||
{
|
{
|
||||||
this->containsRTL = true;
|
this->lineContainsRTL_ = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// check the first non-neutral word to see if we should render RTL or LTR
|
// check the first non-neutral word to see if we should render RTL or LTR
|
||||||
if (isAddingMode && this->first == FirstWord::Neutral &&
|
if (isAddingMode && this->isNeutral() &&
|
||||||
element->getFlags().has(MessageElementFlag::Text) &&
|
element->getFlags().has(MessageElementFlag::Text) &&
|
||||||
!element->getFlags().has(MessageElementFlag::RepliedMessage))
|
!element->getFlags().has(MessageElementFlag::RepliedMessage))
|
||||||
{
|
{
|
||||||
if (element->getText().isRightToLeft())
|
if (isRTLElement)
|
||||||
{
|
{
|
||||||
this->first = FirstWord::RTL;
|
this->textDirection_ = TextDirection::RTL;
|
||||||
}
|
}
|
||||||
else if (!isNeutral(element->getText()))
|
else if (!chatterino::isNeutral(element->getText()))
|
||||||
{
|
{
|
||||||
this->first = FirstWord::LTR;
|
this->textDirection_ = TextDirection::LTR;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -665,7 +674,7 @@ void MessageLayoutContainer::addElement(MessageLayoutElement *element,
|
||||||
shouldRemoveSpaceBetweenEmotes())
|
shouldRemoveSpaceBetweenEmotes())
|
||||||
{
|
{
|
||||||
// Move cursor one 'space width' to the left (right in case of RTL) to combine hug the previous emote
|
// Move cursor one 'space width' to the left (right in case of RTL) to combine hug the previous emote
|
||||||
if (isRTLMode)
|
if (isRTLAdjusting)
|
||||||
{
|
{
|
||||||
this->currentX_ += this->spaceWidth_;
|
this->currentX_ += this->spaceWidth_;
|
||||||
}
|
}
|
||||||
|
@ -675,7 +684,7 @@ void MessageLayoutContainer::addElement(MessageLayoutElement *element,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isRTLMode)
|
if (isRTLAdjusting)
|
||||||
{
|
{
|
||||||
// shift by width since we are calculating according to top right in RTL mode
|
// shift by width since we are calculating according to top right in RTL mode
|
||||||
// but setPosition wants top left
|
// but setPosition wants top left
|
||||||
|
@ -697,7 +706,7 @@ void MessageLayoutContainer::addElement(MessageLayoutElement *element,
|
||||||
}
|
}
|
||||||
|
|
||||||
// set current x
|
// set current x
|
||||||
if (isRTLMode)
|
if (isRTLAdjusting)
|
||||||
{
|
{
|
||||||
this->currentX_ -= element->getRect().width();
|
this->currentX_ -= element->getRect().width();
|
||||||
}
|
}
|
||||||
|
@ -708,7 +717,7 @@ void MessageLayoutContainer::addElement(MessageLayoutElement *element,
|
||||||
|
|
||||||
if (element->hasTrailingSpace())
|
if (element->hasTrailingSpace())
|
||||||
{
|
{
|
||||||
if (isRTLMode)
|
if (isRTLAdjusting)
|
||||||
{
|
{
|
||||||
this->currentX_ -= this->spaceWidth_;
|
this->currentX_ -= this->spaceWidth_;
|
||||||
}
|
}
|
||||||
|
@ -719,15 +728,15 @@ void MessageLayoutContainer::addElement(MessageLayoutElement *element,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void MessageLayoutContainer::reorderRTL(int firstTextIndex)
|
void MessageLayoutContainer::reorderRTL(size_t firstTextIndex)
|
||||||
{
|
{
|
||||||
if (this->elements_.empty())
|
if (this->elements_.empty())
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
int startIndex = static_cast<int>(this->lineStart_);
|
size_t startIndex = this->lineStart_;
|
||||||
int endIndex = static_cast<int>(this->elements_.size()) - 1;
|
size_t endIndex = this->elements_.size() - 1;
|
||||||
|
|
||||||
if (firstTextIndex >= endIndex || startIndex >= this->elements_.size())
|
if (firstTextIndex >= endIndex || startIndex >= this->elements_.size())
|
||||||
{
|
{
|
||||||
|
@ -735,64 +744,53 @@ void MessageLayoutContainer::reorderRTL(int firstTextIndex)
|
||||||
}
|
}
|
||||||
startIndex = std::max(startIndex, firstTextIndex);
|
startIndex = std::max(startIndex, firstTextIndex);
|
||||||
|
|
||||||
std::vector<int> correctSequence;
|
QVarLengthArray<size_t, 32> correctSequence;
|
||||||
std::stack<int> swappedSequence;
|
// temporary buffer to store elements in opposite order
|
||||||
|
QVarLengthArray<size_t, 32> swappedSequence;
|
||||||
|
|
||||||
// we reverse a sequence of words if it's opposite to the text direction
|
bool isReversing = false;
|
||||||
// the second condition below covers the possible three cases:
|
for (size_t i = startIndex; i <= endIndex; i++)
|
||||||
// 1 - if we are in RTL mode (first non-neutral word is RTL)
|
|
||||||
// we render RTL, reversing LTR sequences,
|
|
||||||
// 2 - if we are in LTR mode (first non-neutral word is LTR or all words are neutral)
|
|
||||||
// we render LTR, reversing RTL sequences
|
|
||||||
// 3 - neutral words follow previous words, we reverse a neutral word when the previous word was reversed
|
|
||||||
|
|
||||||
// the first condition checks if a neutral word is treated as a RTL word
|
|
||||||
// this is used later to add U+202B (RTL embedding) character signal to
|
|
||||||
// fix punctuation marks and mixing embedding LTR in an RTL word
|
|
||||||
// this can happen in two cases:
|
|
||||||
// 1 - in RTL mode, the previous word should be RTL (i.e. not reversed)
|
|
||||||
// 2 - in LTR mode, the previous word should be RTL (i.e. reversed)
|
|
||||||
for (int i = startIndex; i <= endIndex; i++)
|
|
||||||
{
|
{
|
||||||
auto &element = this->elements_[i];
|
auto &element = this->elements_[i];
|
||||||
|
|
||||||
const auto neutral = isNeutral(element->getText());
|
const auto neutral = chatterino::isNeutral(element->getText());
|
||||||
const auto neutralOrUsername =
|
const auto neutralOrUsername =
|
||||||
neutral || element->getFlags().has(MessageElementFlag::Mention);
|
neutral || element->getFlags().has(MessageElementFlag::Mention);
|
||||||
|
|
||||||
|
// check if neutral words are treated as RTL to add U+202B (RTL
|
||||||
|
// embedding) which fixes punctuation marks
|
||||||
if (neutral &&
|
if (neutral &&
|
||||||
((this->first == FirstWord::RTL && !this->wasPrevReversed_) ||
|
((this->isRTL() && !isReversing) || (this->isLTR() && isReversing)))
|
||||||
(this->first == FirstWord::LTR && this->wasPrevReversed_)))
|
|
||||||
{
|
{
|
||||||
element->reversedNeutral = true;
|
element->reversedNeutral = true;
|
||||||
}
|
}
|
||||||
if (((element->getText().isRightToLeft() !=
|
|
||||||
(this->first == FirstWord::RTL)) &&
|
if ((element->getText().isRightToLeft() != this->isRTL() &&
|
||||||
!neutralOrUsername) ||
|
!neutralOrUsername) ||
|
||||||
(neutralOrUsername && this->wasPrevReversed_))
|
(neutralOrUsername && isReversing))
|
||||||
{
|
{
|
||||||
swappedSequence.push(i);
|
swappedSequence.append(i);
|
||||||
this->wasPrevReversed_ = true;
|
isReversing = true;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
while (!swappedSequence.empty())
|
while (!swappedSequence.empty())
|
||||||
{
|
{
|
||||||
correctSequence.push_back(swappedSequence.top());
|
correctSequence.push_back(swappedSequence.last());
|
||||||
swappedSequence.pop();
|
swappedSequence.pop_back();
|
||||||
}
|
}
|
||||||
correctSequence.push_back(i);
|
correctSequence.push_back(i);
|
||||||
this->wasPrevReversed_ = false;
|
isReversing = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
while (!swappedSequence.empty())
|
while (!swappedSequence.empty())
|
||||||
{
|
{
|
||||||
correctSequence.push_back(swappedSequence.top());
|
correctSequence.push_back(swappedSequence.last());
|
||||||
swappedSequence.pop();
|
swappedSequence.pop_back();
|
||||||
}
|
}
|
||||||
|
|
||||||
// render right to left if we are in RTL mode, otherwise LTR
|
// render right to left if we are in RTL mode, otherwise LTR
|
||||||
if (this->first == FirstWord::RTL)
|
if (this->isRTL())
|
||||||
{
|
{
|
||||||
this->currentX_ = this->elements_[endIndex]->getRect().right();
|
this->currentX_ = this->elements_[endIndex]->getRect().right();
|
||||||
}
|
}
|
||||||
|
@ -806,10 +804,11 @@ void MessageLayoutContainer::reorderRTL(int firstTextIndex)
|
||||||
this->addElement(this->elements_[correctSequence[0]].get(), false, -1);
|
this->addElement(this->elements_[correctSequence[0]].get(), false, -1);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (int i = 1; i < correctSequence.size() && this->canAddElements(); i++)
|
for (qsizetype i = 1; i < correctSequence.size() && this->canAddElements();
|
||||||
|
i++)
|
||||||
{
|
{
|
||||||
this->addElement(this->elements_[correctSequence[i]].get(), false,
|
this->addElement(this->elements_[correctSequence[i]].get(), false,
|
||||||
correctSequence[i - 1]);
|
static_cast<qsizetype>(correctSequence[i - 1]));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -992,4 +991,19 @@ bool MessageLayoutContainer::canCollapse() const
|
||||||
this->flags_.has(MessageFlag::Collapsed);
|
this->flags_.has(MessageFlag::Collapsed);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool MessageLayoutContainer::isRTL() const noexcept
|
||||||
|
{
|
||||||
|
return this->textDirection_ == TextDirection::RTL;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool MessageLayoutContainer::isLTR() const noexcept
|
||||||
|
{
|
||||||
|
return this->textDirection_ == TextDirection::LTR;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool MessageLayoutContainer::isNeutral() const noexcept
|
||||||
|
{
|
||||||
|
return this->textDirection_ == TextDirection::Neutral;
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace chatterino
|
} // namespace chatterino
|
||||||
|
|
|
@ -10,12 +10,21 @@
|
||||||
#include <optional>
|
#include <optional>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
|
#if __has_include(<gtest/gtest_prod.h>)
|
||||||
|
# include <gtest/gtest_prod.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
class QPainter;
|
class QPainter;
|
||||||
|
|
||||||
namespace chatterino {
|
namespace chatterino {
|
||||||
|
|
||||||
|
enum class TextDirection : uint8_t {
|
||||||
|
Neutral,
|
||||||
|
RTL,
|
||||||
|
LTR,
|
||||||
|
};
|
||||||
|
|
||||||
enum class MessageFlag : int64_t;
|
enum class MessageFlag : int64_t;
|
||||||
enum class FirstWord { Neutral, RTL, LTR };
|
|
||||||
using MessageFlags = FlagsEnum<MessageFlag>;
|
using MessageFlags = FlagsEnum<MessageFlag>;
|
||||||
class MessageLayoutElement;
|
class MessageLayoutElement;
|
||||||
struct Selection;
|
struct Selection;
|
||||||
|
@ -24,8 +33,6 @@ struct MessagePaintContext;
|
||||||
struct MessageLayoutContainer {
|
struct MessageLayoutContainer {
|
||||||
MessageLayoutContainer() = default;
|
MessageLayoutContainer() = default;
|
||||||
|
|
||||||
FirstWord first = FirstWord::Neutral;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Begin the layout process of this message
|
* Begin the layout process of this message
|
||||||
*
|
*
|
||||||
|
@ -212,24 +219,54 @@ private:
|
||||||
QRect rect;
|
QRect rect;
|
||||||
};
|
};
|
||||||
|
|
||||||
/*
|
/// @brief Attempts to add @a element to this container
|
||||||
addElement is called at two stages. first stage is the normal one where we want to add message layout elements to the container.
|
///
|
||||||
If we detect an RTL word in the message, reorderRTL will be called, which is the second stage, where we call _addElement
|
/// This can be called in two scenarios.
|
||||||
again for each layout element, but in the correct order this time, without adding the elemnt to the this->element_ vector.
|
///
|
||||||
Due to compact emote logic, we need the previous element to check if we should change the spacing or not.
|
/// 1. **Regular**: In this scenario, @a element is positioned and added
|
||||||
in stage one, this is simply elements_.back(), but in stage 2 that's not the case due to the reordering, and we need to pass the
|
/// to the internal container.
|
||||||
index of the reordered previous element.
|
/// This is active iff @a prevIndex is `-2`.
|
||||||
In stage one we don't need that and we pass -2 to indicate stage one (i.e. adding mode)
|
/// During this stage, if there isn't any @a textDirection_ detected yet,
|
||||||
In stage two, we pass -1 for the first element, and the index of the oredered privous element for the rest.
|
/// the added element is checked if it contains RTL/LTR text to infer the
|
||||||
*/
|
/// direction. Only upon calling @a breakLine, the elements will be
|
||||||
|
/// visually reorderd.
|
||||||
|
///
|
||||||
|
/// 2. **Repositioning**: In this scenario, @a element is already added to
|
||||||
|
/// the container thus it's only repositioned.
|
||||||
|
/// This is active iff @a prevIndex is not `-2`.
|
||||||
|
/// @a prevIndex is used to handle compact emotes. `-1` is used to
|
||||||
|
/// indicate no predecessor.
|
||||||
|
///
|
||||||
|
/// @param element[in] The element to add. This must be non-null and
|
||||||
|
/// allocated with `new`. Ownership is transferred
|
||||||
|
/// into this container.
|
||||||
|
/// @param forceAdd When enabled, @a element will be added regardless of
|
||||||
|
/// `canAddElements`. If @a element won't be added it will
|
||||||
|
/// be `delete`d.
|
||||||
|
/// @param prevIndex Controls the "scenario" (see above). `-2` indicates
|
||||||
|
/// "regular" mode; other values indicate "repositioning".
|
||||||
|
/// In case of repositioning, this contains the index of
|
||||||
|
/// the precesding element (visually, according to
|
||||||
|
/// @a textDirection_ [RTL/LTR]).
|
||||||
void addElement(MessageLayoutElement *element, bool forceAdd,
|
void addElement(MessageLayoutElement *element, bool forceAdd,
|
||||||
int prevIndex);
|
qsizetype prevIndex);
|
||||||
|
|
||||||
// this method is called when a message has an RTL word
|
/// @brief Reorders the last line according to @a textDirection_
|
||||||
// we need to reorder the words to be shown properly
|
///
|
||||||
// however we don't we to reorder non-text elements like badges, timestamps, username
|
/// If a line contains RTL or the text direction is RTL, elements need to be
|
||||||
// firstTextIndex is the index of the first text element that we need to start the reordering from
|
/// reordered (see @a lineContainsRTL_ and @a isRTL respectively).
|
||||||
void reorderRTL(int firstTextIndex);
|
/// This method reverses sequences of text in the opposite direction for it
|
||||||
|
/// to remain in its intended direction when rendered. Non-text elements
|
||||||
|
/// won't be reordered.
|
||||||
|
///
|
||||||
|
/// For example, in an RTL container, the sequence
|
||||||
|
/// "1_R 2_R 3_N 4_R 5_L 6_L 7_N 8_R" will be (visually) reordered to
|
||||||
|
/// "8_R 5_L 6_L 7_N 4_R 3_N 2_R 1_R" (x_{L,N,R} indicates the element with
|
||||||
|
/// id x which is in the direction {LTR,Neutral,RTL}).
|
||||||
|
///
|
||||||
|
/// @param firstTextIndex The index of the first element of the message
|
||||||
|
/// (i.e. the index after the username).
|
||||||
|
void reorderRTL(size_t firstTextIndex);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Paint a selection rectangle over the given line
|
* Paint a selection rectangle over the given line
|
||||||
|
@ -274,6 +311,15 @@ private:
|
||||||
*/
|
*/
|
||||||
bool canCollapse() const;
|
bool canCollapse() const;
|
||||||
|
|
||||||
|
/// @returns true, if @a textDirection_ is RTL
|
||||||
|
[[nodiscard]] bool isRTL() const noexcept;
|
||||||
|
|
||||||
|
/// @returns true, if @a textDirection_ is LTR
|
||||||
|
[[nodiscard]] bool isLTR() const noexcept;
|
||||||
|
|
||||||
|
/// @returns true, if @a textDirection_ is Neutral
|
||||||
|
[[nodiscard]] bool isNeutral() const noexcept;
|
||||||
|
|
||||||
// variables
|
// variables
|
||||||
float scale_ = 1.F;
|
float scale_ = 1.F;
|
||||||
/**
|
/**
|
||||||
|
@ -304,13 +350,18 @@ private:
|
||||||
int currentWordId_ = 0;
|
int currentWordId_ = 0;
|
||||||
bool canAddMessages_ = true;
|
bool canAddMessages_ = true;
|
||||||
bool isCollapsed_ = false;
|
bool isCollapsed_ = false;
|
||||||
bool wasPrevReversed_ = false;
|
|
||||||
|
|
||||||
/**
|
/// @brief True if the current line contains any RTL text.
|
||||||
* containsRTL indicates whether or not any of the text in this message
|
///
|
||||||
* contains any right-to-left characters (e.g. arabic)
|
/// If the line contains any RTL, it needs to be reordered after a
|
||||||
*/
|
/// linebreak after which it's reset to `false`.
|
||||||
bool containsRTL = false;
|
bool lineContainsRTL_ = false;
|
||||||
|
|
||||||
|
/// @brief The direction of the text in this container.
|
||||||
|
///
|
||||||
|
/// This starts off as neutral until an element is encountered that is
|
||||||
|
/// either LTR or RTL (afterwards this remains constant).
|
||||||
|
TextDirection textDirection_ = TextDirection::Neutral;
|
||||||
|
|
||||||
std::vector<std::unique_ptr<MessageLayoutElement>> elements_;
|
std::vector<std::unique_ptr<MessageLayoutElement>> elements_;
|
||||||
|
|
||||||
|
@ -320,6 +371,10 @@ private:
|
||||||
* These lines hold no relation to the elements that are in this
|
* These lines hold no relation to the elements that are in this
|
||||||
*/
|
*/
|
||||||
std::vector<Line> lines_;
|
std::vector<Line> lines_;
|
||||||
|
|
||||||
|
#ifdef FRIEND_TEST
|
||||||
|
FRIEND_TEST(MessageLayoutContainerTest, RtlReordering);
|
||||||
|
#endif
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace chatterino
|
} // namespace chatterino
|
||||||
|
|
|
@ -46,6 +46,7 @@ set(test_SOURCES
|
||||||
${CMAKE_CURRENT_LIST_DIR}/src/Scrollbar.cpp
|
${CMAKE_CURRENT_LIST_DIR}/src/Scrollbar.cpp
|
||||||
${CMAKE_CURRENT_LIST_DIR}/src/Commands.cpp
|
${CMAKE_CURRENT_LIST_DIR}/src/Commands.cpp
|
||||||
${CMAKE_CURRENT_LIST_DIR}/src/FlagsEnum.cpp
|
${CMAKE_CURRENT_LIST_DIR}/src/FlagsEnum.cpp
|
||||||
|
${CMAKE_CURRENT_LIST_DIR}/src/MessageLayoutContainer.cpp
|
||||||
# Add your new file above this line!
|
# Add your new file above this line!
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
237
tests/src/MessageLayoutContainer.cpp
Normal file
237
tests/src/MessageLayoutContainer.cpp
Normal file
|
@ -0,0 +1,237 @@
|
||||||
|
#include "messages/layouts/MessageLayoutContainer.hpp"
|
||||||
|
|
||||||
|
#include "common/Literals.hpp"
|
||||||
|
#include "messages/Emote.hpp"
|
||||||
|
#include "messages/layouts/MessageLayoutElement.hpp"
|
||||||
|
#include "messages/Message.hpp"
|
||||||
|
#include "messages/MessageElement.hpp"
|
||||||
|
#include "mocks/BaseApplication.hpp"
|
||||||
|
#include "singletons/Fonts.hpp"
|
||||||
|
#include "singletons/Resources.hpp"
|
||||||
|
#include "singletons/Theme.hpp"
|
||||||
|
#include "Test.hpp"
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
using namespace chatterino;
|
||||||
|
using namespace literals;
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
class MockApplication : mock::BaseApplication
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
MockApplication()
|
||||||
|
: theme(this->paths_)
|
||||||
|
, fonts(this->settings)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
Theme *getThemes() override
|
||||||
|
{
|
||||||
|
return &this->theme;
|
||||||
|
}
|
||||||
|
|
||||||
|
Fonts *getFonts() override
|
||||||
|
{
|
||||||
|
return &this->fonts;
|
||||||
|
}
|
||||||
|
|
||||||
|
Theme theme;
|
||||||
|
Fonts fonts;
|
||||||
|
};
|
||||||
|
|
||||||
|
std::vector<std::shared_ptr<MessageElement>> makeElements(const QString &text)
|
||||||
|
{
|
||||||
|
std::vector<std::shared_ptr<MessageElement>> elements;
|
||||||
|
bool seenUsername = false;
|
||||||
|
for (const auto &word : text.split(' '))
|
||||||
|
{
|
||||||
|
if (word.startsWith('@'))
|
||||||
|
{
|
||||||
|
if (seenUsername)
|
||||||
|
{
|
||||||
|
elements.emplace_back(std::make_shared<MentionElement>(
|
||||||
|
word, word, MessageColor{}, MessageColor{}));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
elements.emplace_back(std::make_shared<TextElement>(
|
||||||
|
word, MessageElementFlag::Username, MessageColor{},
|
||||||
|
FontStyle::ChatMediumBold));
|
||||||
|
seenUsername = true;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (word.startsWith('!'))
|
||||||
|
{
|
||||||
|
auto emote = std::make_shared<Emote>(Emote{
|
||||||
|
.name = EmoteName{word},
|
||||||
|
.images = ImageSet{Image::fromResourcePixmap(
|
||||||
|
getResources().buttons.addSplit)},
|
||||||
|
.tooltip = {},
|
||||||
|
.homePage = {},
|
||||||
|
.id = {},
|
||||||
|
.author = {},
|
||||||
|
.baseName = {},
|
||||||
|
});
|
||||||
|
elements.emplace_back(std::make_shared<EmoteElement>(
|
||||||
|
emote, MessageElementFlag::TwitchEmote));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
elements.emplace_back(std::make_shared<TextElement>(
|
||||||
|
word, MessageElementFlag::Text, MessageColor{},
|
||||||
|
FontStyle::ChatMedium));
|
||||||
|
}
|
||||||
|
|
||||||
|
return elements;
|
||||||
|
}
|
||||||
|
|
||||||
|
using TestParam = std::tuple<QString, QString, TextDirection>;
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
namespace chatterino {
|
||||||
|
|
||||||
|
class MessageLayoutContainerTest : public ::testing::TestWithParam<TestParam>
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
MessageLayoutContainerTest() = default;
|
||||||
|
|
||||||
|
MockApplication mockApplication;
|
||||||
|
};
|
||||||
|
|
||||||
|
TEST_P(MessageLayoutContainerTest, RtlReordering)
|
||||||
|
{
|
||||||
|
auto [inputText, expected, expectedDirection] = GetParam();
|
||||||
|
MessageLayoutContainer container;
|
||||||
|
container.beginLayout(10000, 1.0F, 1.0F, {MessageFlag::Collapsed});
|
||||||
|
|
||||||
|
auto elements = makeElements(inputText);
|
||||||
|
for (const auto &element : elements)
|
||||||
|
{
|
||||||
|
element->addToContainer(container, {
|
||||||
|
MessageElementFlag::Text,
|
||||||
|
MessageElementFlag::Username,
|
||||||
|
MessageElementFlag::TwitchEmote,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
container.breakLine();
|
||||||
|
ASSERT_EQ(container.line_, 1) << "unexpected linebreak";
|
||||||
|
|
||||||
|
// message layout elements ordered by x position
|
||||||
|
std::vector<MessageLayoutElement *> ordered;
|
||||||
|
ordered.reserve(container.elements_.size());
|
||||||
|
for (const auto &el : container.elements_)
|
||||||
|
{
|
||||||
|
ordered.push_back(el.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
std::ranges::sort(ordered, [](auto *a, auto *b) {
|
||||||
|
return a->getRect().x() < b->getRect().x();
|
||||||
|
});
|
||||||
|
|
||||||
|
QString got;
|
||||||
|
for (const auto &el : ordered)
|
||||||
|
{
|
||||||
|
if (!got.isNull())
|
||||||
|
{
|
||||||
|
got.append(' ');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dynamic_cast<ImageLayoutElement *>(el))
|
||||||
|
{
|
||||||
|
el->addCopyTextToString(got);
|
||||||
|
ASSERT_TRUE(got.endsWith(' '));
|
||||||
|
got.chop(1);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
got.append(el->getText());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ASSERT_EQ(got, expected) << got;
|
||||||
|
ASSERT_EQ(container.textDirection_, expectedDirection) << got;
|
||||||
|
}
|
||||||
|
|
||||||
|
INSTANTIATE_TEST_SUITE_P(
|
||||||
|
MessageLayoutContainer, MessageLayoutContainerTest,
|
||||||
|
testing::Values(
|
||||||
|
TestParam{
|
||||||
|
u"@aliens foo bar baz @foo qox !emote1 !emote2"_s,
|
||||||
|
u"@aliens foo bar baz @foo qox !emote1 !emote2"_s,
|
||||||
|
TextDirection::LTR,
|
||||||
|
},
|
||||||
|
TestParam{
|
||||||
|
u"@aliens ! foo bar baz @foo qox !emote1 !emote2"_s,
|
||||||
|
u"@aliens ! foo bar baz @foo qox !emote1 !emote2"_s,
|
||||||
|
TextDirection::LTR,
|
||||||
|
},
|
||||||
|
TestParam{
|
||||||
|
u"@aliens ."_s,
|
||||||
|
u"@aliens ."_s,
|
||||||
|
TextDirection::Neutral,
|
||||||
|
},
|
||||||
|
// RTL
|
||||||
|
TestParam{
|
||||||
|
u"@aliens و غير دارت إعادة, بل كما وقام قُدُماً. قام تم الجوي بوابة, خلاف أراض هو بلا. عن وحتّى ميناء غير"_s,
|
||||||
|
u"@aliens غير ميناء وحتّى عن بلا. هو أراض خلاف بوابة, الجوي تم قام قُدُماً. وقام كما بل إعادة, دارت غير و"_s,
|
||||||
|
TextDirection::RTL,
|
||||||
|
},
|
||||||
|
TestParam{
|
||||||
|
u"@aliens و غير دارت إعادة, بل ض هو my LTR 123 بلا. عن 123 456 وحتّى ميناء غير"_s,
|
||||||
|
u"@aliens غير ميناء وحتّى 456 123 عن بلا. my LTR 123 هو ض بل إعادة, دارت غير و"_s,
|
||||||
|
TextDirection::RTL,
|
||||||
|
},
|
||||||
|
TestParam{
|
||||||
|
u"@aliens ور دارت إ @user baz bar عاد هو my LTR 123 بلا. عن 123 456 وحتّ غير"_s,
|
||||||
|
u"@aliens غير وحتّ 456 123 عن بلا. my LTR 123 هو عاد baz bar @user إ دارت ور"_s,
|
||||||
|
TextDirection::RTL,
|
||||||
|
},
|
||||||
|
TestParam{
|
||||||
|
u"@aliens ور !emote1 !emote2 !emote3 دارت إ @user baz bar عاد هو my LTR 123 بلا. عن 123 456 وحتّ غير"_s,
|
||||||
|
u"@aliens غير وحتّ 456 123 عن بلا. my LTR 123 هو عاد baz bar @user إ دارت !emote3 !emote2 !emote1 ور"_s,
|
||||||
|
TextDirection::RTL,
|
||||||
|
},
|
||||||
|
TestParam{
|
||||||
|
u"@aliens ور !emote1 !emote2 LTR text !emote3 !emote4 غير"_s,
|
||||||
|
u"@aliens غير LTR text !emote3 !emote4 !emote2 !emote1 ور"_s,
|
||||||
|
TextDirection::RTL,
|
||||||
|
},
|
||||||
|
|
||||||
|
TestParam{
|
||||||
|
u"@aliens !!! ور !emote1 !emote2 LTR text !emote3 !emote4 غير"_s,
|
||||||
|
u"@aliens غير LTR text !emote3 !emote4 !emote2 !emote1 ور !!!"_s,
|
||||||
|
TextDirection::RTL,
|
||||||
|
},
|
||||||
|
// LTR
|
||||||
|
TestParam{
|
||||||
|
u"@aliens LTR و غير دا ميناء غير"_s,
|
||||||
|
u"@aliens LTR غير ميناء دا غير و"_s,
|
||||||
|
TextDirection::LTR,
|
||||||
|
},
|
||||||
|
TestParam{
|
||||||
|
u"@aliens LTR و غير د ض هو my LTR 123 بلا. عن 123 456 وحتّى مير"_s,
|
||||||
|
u"@aliens LTR هو ض د غير و my LTR 123 مير وحتّى 456 123 عن بلا."_s,
|
||||||
|
TextDirection::LTR,
|
||||||
|
},
|
||||||
|
TestParam{
|
||||||
|
u"@aliens LTR ور دارت إ @user baz bar عاد هو my LTR 123 بلا. عن 123 456 وحتّ غير"_s,
|
||||||
|
u"@aliens LTR @user إ دارت ور baz bar هو عاد my LTR 123 غير وحتّ 456 123 عن بلا."_s,
|
||||||
|
TextDirection::LTR,
|
||||||
|
},
|
||||||
|
TestParam{
|
||||||
|
u"@aliens LTR ور !emote1 !emote2 !emote3 دارت إ @user baz bar عاد هو my LTR 123 بلا. عن 123 456 وحتّ غير"_s,
|
||||||
|
u"@aliens LTR @user إ دارت !emote3 !emote2 !emote1 ور baz bar هو عاد my LTR 123 غير وحتّ 456 123 عن بلا."_s,
|
||||||
|
TextDirection::LTR,
|
||||||
|
},
|
||||||
|
TestParam{
|
||||||
|
u"@aliens LTR غير وحتّ !emote1 !emote2 LTR text !emote3 !emote4 عاد هو"_s,
|
||||||
|
u"@aliens LTR !emote2 !emote1 وحتّ غير LTR text !emote3 !emote4 هو عاد"_s,
|
||||||
|
TextDirection::LTR,
|
||||||
|
}));
|
||||||
|
|
||||||
|
} // namespace chatterino
|
Loading…
Reference in a new issue