Implement initial support for RTL languages (#3958)

Co-authored-by: pajlada <rasmus.karlsson@pajlada.com>
fix https://github.com/Chatterino/chatterino2/issues/720
This commit is contained in:
mohad12211
2022-11-10 23:36:19 +03:00
committed by GitHub
parent fbfa5e0f41
commit 3fcb7e1702
8 changed files with 217 additions and 13 deletions
+165 -10
View File
@@ -8,6 +8,7 @@
#include "singletons/Fonts.hpp"
#include "singletons/Settings.hpp"
#include "singletons/Theme.hpp"
#include "util/Helpers.hpp"
#include <QDebug>
#include <QPainter>
@@ -88,7 +89,7 @@ bool MessageLayoutContainer::canAddElements()
}
void MessageLayoutContainer::_addElement(MessageLayoutElement *element,
bool forceAdd)
bool forceAdd, int prevIndex)
{
if (!this->canAddElements() && !forceAdd)
{
@@ -96,15 +97,19 @@ void MessageLayoutContainer::_addElement(MessageLayoutElement *element,
return;
}
bool isRTLMode = this->first == FirstWord::RTL && prevIndex != -2;
bool isAddingMode = prevIndex == -2;
// This lambda contains the logic for when to step one 'space width' back for compact x emotes
auto shouldRemoveSpaceBetweenEmotes = [this]() -> bool {
if (this->elements_.empty())
auto shouldRemoveSpaceBetweenEmotes = [this, prevIndex]() -> bool {
if (prevIndex == -1 || this->elements_.empty())
{
// No previous element found
return false;
}
const auto &lastElement = this->elements_.back();
const auto &lastElement = prevIndex == -2 ? this->elements_.back()
: this->elements_[prevIndex];
if (!lastElement)
{
@@ -127,6 +132,26 @@ void MessageLayoutContainer::_addElement(MessageLayoutElement *element,
return lastElement->getFlags().has(MessageElementFlag::EmoteImages);
};
if (element->getText().isRightToLeft())
{
this->containsRTL = true;
}
// check the first non-neutral word to see if we should render RTL or LTR
if (isAddingMode && this->first == FirstWord::Neutral &&
element->getFlags().has(MessageElementFlag::Text) &&
!element->getFlags().has(MessageElementFlag::RepliedMessage))
{
if (element->getText().isRightToLeft())
{
this->first = FirstWord::RTL;
}
else if (!isNeutral(element->getText()))
{
this->first = FirstWord::LTR;
}
}
// top margin
if (this->elements_.size() == 0)
{
@@ -152,7 +177,7 @@ void MessageLayoutContainer::_addElement(MessageLayoutElement *element,
bool isZeroWidthEmote = element->getCreator().getFlags().has(
MessageElementFlag::ZeroWidthEmote);
if (isZeroWidthEmote)
if (isZeroWidthEmote && !isRTLMode)
{
xOffset -= element->getRect().width() + this->spaceWidth_;
}
@@ -171,8 +196,22 @@ void MessageLayoutContainer::_addElement(MessageLayoutElement *element,
element->getFlags().hasAny({MessageElementFlag::EmoteImages}) &&
!isZeroWidthEmote && shouldRemoveSpaceBetweenEmotes())
{
// Move cursor one 'space width' to the left to combine hug the previous emote
this->currentX_ -= this->spaceWidth_;
// Move cursor one 'space width' to the left (right in case of RTL) to combine hug the previous emote
if (isRTLMode)
{
this->currentX_ += this->spaceWidth_;
}
else
{
this->currentX_ -= this->spaceWidth_;
}
}
if (isRTLMode)
{
// shift by width since we are calculating according to top right in RTL mode
// but setPosition wants top left
xOffset -= element->getRect().width();
}
// set move element
@@ -183,22 +222,138 @@ void MessageLayoutContainer::_addElement(MessageLayoutElement *element,
element->setLine(this->line_);
// add element
this->elements_.push_back(std::unique_ptr<MessageLayoutElement>(element));
if (isAddingMode)
{
this->elements_.push_back(
std::unique_ptr<MessageLayoutElement>(element));
}
// set current x
if (!isZeroWidthEmote)
{
this->currentX_ += element->getRect().width();
if (isRTLMode)
{
this->currentX_ -= element->getRect().width();
}
else
{
this->currentX_ += element->getRect().width();
}
}
if (element->hasTrailingSpace())
{
this->currentX_ += this->spaceWidth_;
if (isRTLMode)
{
this->currentX_ -= this->spaceWidth_;
}
else
{
this->currentX_ += this->spaceWidth_;
}
}
}
void MessageLayoutContainer::reorderRTL(int firstTextIndex)
{
if (this->elements_.empty())
{
return;
}
int startIndex = static_cast<int>(this->lineStart_);
int endIndex = static_cast<int>(this->elements_.size()) - 1;
if (firstTextIndex >= endIndex)
{
return;
}
startIndex = std::max(startIndex, firstTextIndex);
std::vector<int> correctSequence;
std::stack<int> swappedSequence;
bool wasPrevReversed = false;
// 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-neautral word is LTR or all wrods 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 an invisible Arabic letter to fix orentation
// 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++)
{
if (isNeutral(this->elements_[i]->getText()) &&
((this->first == FirstWord::RTL && !wasPrevReversed) ||
(this->first == FirstWord::LTR && wasPrevReversed)))
{
this->elements_[i]->reversedNeutral = true;
}
if (((this->elements_[i]->getText().isRightToLeft() !=
(this->first == FirstWord::RTL)) &&
!isNeutral(this->elements_[i]->getText())) ||
(isNeutral(this->elements_[i]->getText()) && wasPrevReversed))
{
swappedSequence.push(i);
wasPrevReversed = true;
}
else
{
while (!swappedSequence.empty())
{
correctSequence.push_back(swappedSequence.top());
swappedSequence.pop();
}
correctSequence.push_back(i);
wasPrevReversed = false;
}
}
while (!swappedSequence.empty())
{
correctSequence.push_back(swappedSequence.top());
swappedSequence.pop();
}
// render right to left if we are in RTL mode, otherwise LTR
if (this->first == FirstWord::RTL)
{
this->currentX_ = this->elements_[endIndex]->getRect().right();
}
else
{
this->currentX_ = this->elements_[startIndex]->getRect().left();
}
// manually do the first call with -1 as previous index
this->_addElement(this->elements_[correctSequence[0]].get(), false, -1);
for (int i = 1; i < correctSequence.size(); i++)
{
this->_addElement(this->elements_[correctSequence[i]].get(), false,
correctSequence[i - 1]);
}
}
void MessageLayoutContainer::breakLine()
{
if (this->containsRTL)
{
for (int i = 0; i < this->elements_.size(); i++)
{
if (this->elements_[i]->getFlags().has(
MessageElementFlag::Username))
{
this->reorderRTL(i + 1);
break;
}
}
}
int xOffset = 0;
if (this->flags_.has(MessageFlag::Centered) && this->elements_.size() > 0)