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: 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: Documented and added tests to RTL handling. (#5473)
|
||||
|
||||
## 2.5.1
|
||||
|
||||
|
|
|
@ -53,24 +53,27 @@ Frames::Frames(QList<Frame> &&frames)
|
|||
getApp()->getEmotes()->getGIFTimer().signal.connect([this] {
|
||||
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 (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. */
|
||||
QString currentText;
|
||||
|
||||
container.first = FirstWord::Neutral;
|
||||
|
||||
bool firstIteration = true;
|
||||
for (Word &word : this->words_)
|
||||
{
|
||||
|
|
|
@ -119,9 +119,9 @@ private:
|
|||
QPixmap *ensureBuffer(QPainter &painter, int width);
|
||||
|
||||
// variables
|
||||
MessagePtr message_;
|
||||
const MessagePtr message_;
|
||||
MessageLayoutContainer container_;
|
||||
std::unique_ptr<QPixmap> buffer_{};
|
||||
std::unique_ptr<QPixmap> buffer_;
|
||||
bool bufferValid_ = false;
|
||||
|
||||
int height_ = 0;
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
#include "MessageLayoutContainer.hpp"
|
||||
#include "messages/layouts/MessageLayoutContainer.hpp"
|
||||
|
||||
#include "Application.hpp"
|
||||
#include "messages/layouts/MessageLayoutContext.hpp"
|
||||
|
@ -14,6 +14,7 @@
|
|||
#include <QDebug>
|
||||
#include <QMargins>
|
||||
#include <QPainter>
|
||||
#include <QVarLengthArray>
|
||||
|
||||
#include <optional>
|
||||
|
||||
|
@ -55,7 +56,6 @@ void MessageLayoutContainer::beginLayout(int width, float scale,
|
|||
this->currentWordId_ = 0;
|
||||
this->canAddMessages_ = true;
|
||||
this->isCollapsed_ = false;
|
||||
this->wasPrevReversed_ = false;
|
||||
}
|
||||
|
||||
void MessageLayoutContainer::endLayout()
|
||||
|
@ -71,7 +71,7 @@ void MessageLayoutContainer::endLayout()
|
|||
QSize(this->dotdotdotWidth_, this->textLineHeight_),
|
||||
QColor("#00D80A"), FontStyle::ChatMediumBold, this->scale_);
|
||||
|
||||
if (this->first == FirstWord::RTL)
|
||||
if (this->isRTL())
|
||||
{
|
||||
// Shift all elements in the next line to the left
|
||||
for (auto i = this->lines_.back().startIndex;
|
||||
|
@ -125,9 +125,9 @@ void MessageLayoutContainer::addElementNoLineBreak(
|
|||
|
||||
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(
|
||||
MessageElementFlag::Username))
|
||||
|
@ -136,6 +136,7 @@ void MessageLayoutContainer::breakLine()
|
|||
break;
|
||||
}
|
||||
}
|
||||
this->lineContainsRTL_ = false;
|
||||
}
|
||||
|
||||
int xOffset = 0;
|
||||
|
@ -404,7 +405,7 @@ size_t MessageLayoutContainer::getSelectionIndex(QPoint point) const
|
|||
|
||||
size_t index = 0;
|
||||
|
||||
for (auto i = 0; i < lineEnd; i++)
|
||||
for (size_t i = 0; i < lineEnd; i++)
|
||||
{
|
||||
auto &&element = this->elements_[i];
|
||||
|
||||
|
@ -565,30 +566,37 @@ int MessageLayoutContainer::nextWordId()
|
|||
|
||||
void MessageLayoutContainer::addElement(MessageLayoutElement *element,
|
||||
const bool forceAdd,
|
||||
const int prevIndex)
|
||||
const qsizetype prevIndex)
|
||||
{
|
||||
if (!this->canAddElements() && !forceAdd)
|
||||
{
|
||||
assert(prevIndex == -2 &&
|
||||
"element is still referenced in this->elements_");
|
||||
delete element;
|
||||
return;
|
||||
}
|
||||
|
||||
bool isRTLMode = this->first == FirstWord::RTL && 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
|
||||
auto shouldRemoveSpaceBetweenEmotes = [this, prevIndex]() -> bool {
|
||||
/// Returns `true` if a previously added `spaceWidth_` should be removed
|
||||
/// 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())
|
||||
{
|
||||
// No previous element found
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto &lastElement = prevIndex == -2 ? this->elements_.back()
|
||||
: this->elements_[prevIndex];
|
||||
const auto &lastElement =
|
||||
isAddingMode ? this->elements_.back() : this->elements_[prevIndex];
|
||||
|
||||
if (!lastElement)
|
||||
{
|
||||
assert(false && "Empty element in container found");
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -608,23 +616,24 @@ void MessageLayoutContainer::addElement(MessageLayoutElement *element,
|
|||
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
|
||||
if (isAddingMode && this->first == FirstWord::Neutral &&
|
||||
if (isAddingMode && this->isNeutral() &&
|
||||
element->getFlags().has(MessageElementFlag::Text) &&
|
||||
!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())
|
||||
{
|
||||
// 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_;
|
||||
}
|
||||
|
@ -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
|
||||
// but setPosition wants top left
|
||||
|
@ -697,7 +706,7 @@ void MessageLayoutContainer::addElement(MessageLayoutElement *element,
|
|||
}
|
||||
|
||||
// set current x
|
||||
if (isRTLMode)
|
||||
if (isRTLAdjusting)
|
||||
{
|
||||
this->currentX_ -= element->getRect().width();
|
||||
}
|
||||
|
@ -708,7 +717,7 @@ void MessageLayoutContainer::addElement(MessageLayoutElement *element,
|
|||
|
||||
if (element->hasTrailingSpace())
|
||||
{
|
||||
if (isRTLMode)
|
||||
if (isRTLAdjusting)
|
||||
{
|
||||
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())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
int startIndex = static_cast<int>(this->lineStart_);
|
||||
int endIndex = static_cast<int>(this->elements_.size()) - 1;
|
||||
size_t startIndex = this->lineStart_;
|
||||
size_t endIndex = this->elements_.size() - 1;
|
||||
|
||||
if (firstTextIndex >= endIndex || startIndex >= this->elements_.size())
|
||||
{
|
||||
|
@ -735,64 +744,53 @@ void MessageLayoutContainer::reorderRTL(int firstTextIndex)
|
|||
}
|
||||
startIndex = std::max(startIndex, firstTextIndex);
|
||||
|
||||
std::vector<int> correctSequence;
|
||||
std::stack<int> swappedSequence;
|
||||
QVarLengthArray<size_t, 32> correctSequence;
|
||||
// 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
|
||||
// the second condition below covers the possible three cases:
|
||||
// 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++)
|
||||
bool isReversing = false;
|
||||
for (size_t i = startIndex; i <= endIndex; i++)
|
||||
{
|
||||
auto &element = this->elements_[i];
|
||||
|
||||
const auto neutral = isNeutral(element->getText());
|
||||
const auto neutral = chatterino::isNeutral(element->getText());
|
||||
const auto neutralOrUsername =
|
||||
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 &&
|
||||
((this->first == FirstWord::RTL && !this->wasPrevReversed_) ||
|
||||
(this->first == FirstWord::LTR && this->wasPrevReversed_)))
|
||||
((this->isRTL() && !isReversing) || (this->isLTR() && isReversing)))
|
||||
{
|
||||
element->reversedNeutral = true;
|
||||
}
|
||||
if (((element->getText().isRightToLeft() !=
|
||||
(this->first == FirstWord::RTL)) &&
|
||||
|
||||
if ((element->getText().isRightToLeft() != this->isRTL() &&
|
||||
!neutralOrUsername) ||
|
||||
(neutralOrUsername && this->wasPrevReversed_))
|
||||
(neutralOrUsername && isReversing))
|
||||
{
|
||||
swappedSequence.push(i);
|
||||
this->wasPrevReversed_ = true;
|
||||
swappedSequence.append(i);
|
||||
isReversing = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
while (!swappedSequence.empty())
|
||||
{
|
||||
correctSequence.push_back(swappedSequence.top());
|
||||
swappedSequence.pop();
|
||||
correctSequence.push_back(swappedSequence.last());
|
||||
swappedSequence.pop_back();
|
||||
}
|
||||
correctSequence.push_back(i);
|
||||
this->wasPrevReversed_ = false;
|
||||
isReversing = false;
|
||||
}
|
||||
}
|
||||
while (!swappedSequence.empty())
|
||||
{
|
||||
correctSequence.push_back(swappedSequence.top());
|
||||
swappedSequence.pop();
|
||||
correctSequence.push_back(swappedSequence.last());
|
||||
swappedSequence.pop_back();
|
||||
}
|
||||
|
||||
// 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();
|
||||
}
|
||||
|
@ -806,10 +804,11 @@ void MessageLayoutContainer::reorderRTL(int firstTextIndex)
|
|||
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,
|
||||
correctSequence[i - 1]);
|
||||
static_cast<qsizetype>(correctSequence[i - 1]));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -992,4 +991,19 @@ bool MessageLayoutContainer::canCollapse() const
|
|||
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
|
||||
|
|
|
@ -10,12 +10,21 @@
|
|||
#include <optional>
|
||||
#include <vector>
|
||||
|
||||
#if __has_include(<gtest/gtest_prod.h>)
|
||||
# include <gtest/gtest_prod.h>
|
||||
#endif
|
||||
|
||||
class QPainter;
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
enum class TextDirection : uint8_t {
|
||||
Neutral,
|
||||
RTL,
|
||||
LTR,
|
||||
};
|
||||
|
||||
enum class MessageFlag : int64_t;
|
||||
enum class FirstWord { Neutral, RTL, LTR };
|
||||
using MessageFlags = FlagsEnum<MessageFlag>;
|
||||
class MessageLayoutElement;
|
||||
struct Selection;
|
||||
|
@ -24,8 +33,6 @@ struct MessagePaintContext;
|
|||
struct MessageLayoutContainer {
|
||||
MessageLayoutContainer() = default;
|
||||
|
||||
FirstWord first = FirstWord::Neutral;
|
||||
|
||||
/**
|
||||
* Begin the layout process of this message
|
||||
*
|
||||
|
@ -212,24 +219,54 @@ private:
|
|||
QRect rect;
|
||||
};
|
||||
|
||||
/*
|
||||
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
|
||||
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.
|
||||
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
|
||||
index of the reordered previous element.
|
||||
In stage one we don't need that and we pass -2 to indicate stage one (i.e. adding mode)
|
||||
In stage two, we pass -1 for the first element, and the index of the oredered privous element for the rest.
|
||||
*/
|
||||
/// @brief Attempts to add @a element to this container
|
||||
///
|
||||
/// This can be called in two scenarios.
|
||||
///
|
||||
/// 1. **Regular**: In this scenario, @a element is positioned and added
|
||||
/// to the internal container.
|
||||
/// This is active iff @a prevIndex is `-2`.
|
||||
/// During this stage, if there isn't any @a textDirection_ detected yet,
|
||||
/// 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,
|
||||
int prevIndex);
|
||||
qsizetype prevIndex);
|
||||
|
||||
// this method is called when a message has an RTL word
|
||||
// we need to reorder the words to be shown properly
|
||||
// however we don't we to reorder non-text elements like badges, timestamps, username
|
||||
// firstTextIndex is the index of the first text element that we need to start the reordering from
|
||||
void reorderRTL(int firstTextIndex);
|
||||
/// @brief Reorders the last line according to @a textDirection_
|
||||
///
|
||||
/// If a line contains RTL or the text direction is RTL, elements need to be
|
||||
/// reordered (see @a lineContainsRTL_ and @a isRTL respectively).
|
||||
/// 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
|
||||
|
@ -274,6 +311,15 @@ private:
|
|||
*/
|
||||
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
|
||||
float scale_ = 1.F;
|
||||
/**
|
||||
|
@ -304,13 +350,18 @@ private:
|
|||
int currentWordId_ = 0;
|
||||
bool canAddMessages_ = true;
|
||||
bool isCollapsed_ = false;
|
||||
bool wasPrevReversed_ = false;
|
||||
|
||||
/**
|
||||
* containsRTL indicates whether or not any of the text in this message
|
||||
* contains any right-to-left characters (e.g. arabic)
|
||||
*/
|
||||
bool containsRTL = false;
|
||||
/// @brief True if the current line contains any RTL text.
|
||||
///
|
||||
/// If the line contains any RTL, it needs to be reordered after a
|
||||
/// linebreak after which it's reset to `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_;
|
||||
|
||||
|
@ -320,6 +371,10 @@ private:
|
|||
* These lines hold no relation to the elements that are in this
|
||||
*/
|
||||
std::vector<Line> lines_;
|
||||
|
||||
#ifdef FRIEND_TEST
|
||||
FRIEND_TEST(MessageLayoutContainerTest, RtlReordering);
|
||||
#endif
|
||||
};
|
||||
|
||||
} // namespace chatterino
|
||||
|
|
|
@ -46,6 +46,7 @@ set(test_SOURCES
|
|||
${CMAKE_CURRENT_LIST_DIR}/src/Scrollbar.cpp
|
||||
${CMAKE_CURRENT_LIST_DIR}/src/Commands.cpp
|
||||
${CMAKE_CURRENT_LIST_DIR}/src/FlagsEnum.cpp
|
||||
${CMAKE_CURRENT_LIST_DIR}/src/MessageLayoutContainer.cpp
|
||||
# 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