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:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user