diff --git a/Common/Data/Encoding/Utf8.cpp b/Common/Data/Encoding/Utf8.cpp index 8ec109d423..dec734da55 100644 --- a/Common/Data/Encoding/Utf8.cpp +++ b/Common/Data/Encoding/Utf8.cpp @@ -169,6 +169,24 @@ bool UTF8StringHasNonASCII(std::string_view utf8string) { return UTF8StringNonASCIICount(utf8string) > 0; } +static bool CodepointIsRTL(uint32_t c) { + return (c >= 0x0590 && c <= 0x08FF) || // Hebrew, Arabic, Syriac, Thaana, NKo, ... + (c >= 0xFB1D && c <= 0xFDFF) || // Hebrew and Arabic presentation forms + (c >= 0xFE70 && c <= 0xFEFF) || + (c >= 0x10800 && c <= 0x10FFF) || + (c >= 0x1E800 && c <= 0x1EFFF); +} + +bool UTF8StringHasRTL(std::string_view utf8string) { + UTF8 utf(utf8string); + while (!utf.end()) { + if (CodepointIsRTL(utf.next())) { + return true; + } + } + return false; +} + #ifdef _WIN32 std::string ConvertWStringToUTF8(const wchar_t *wstr) { diff --git a/Common/Data/Encoding/Utf8.h b/Common/Data/Encoding/Utf8.h index 81363f06e5..ebc36ce510 100644 --- a/Common/Data/Encoding/Utf8.h +++ b/Common/Data/Encoding/Utf8.h @@ -92,6 +92,8 @@ private: int UTF8StringNonASCIICount(std::string_view utf8string); bool UTF8StringHasNonASCII(std::string_view utf8string); +// True if the string contains a character from a right-to-left script (Hebrew, Arabic, ...). +bool UTF8StringHasRTL(std::string_view utf8string); // Removes overlong encodings and similar. diff --git a/Common/Data/Text/I18n.cpp b/Common/Data/Text/I18n.cpp index e9f1683047..82c4cea8c4 100644 --- a/Common/Data/Text/I18n.cpp +++ b/Common/Data/Text/I18n.cpp @@ -50,6 +50,10 @@ std::string I18NRepo::LanguageID() { return languageID_; } +bool I18NRepo::IsRTL() const { + return languageID_ == "he_IL" || languageID_ == "ar_AE" || languageID_ == "fa_IR" || languageID_ == "ku_SO"; +} + I18NRepo::I18NRepo() { Clear(); } diff --git a/Common/Data/Text/I18n.h b/Common/Data/Text/I18n.h index 9d1c98045f..3b14f5ffcb 100644 --- a/Common/Data/Text/I18n.h +++ b/Common/Data/Text/I18n.h @@ -108,6 +108,8 @@ public: bool LoadIni(const std::string &languageID, const Path &overridePath = Path()); // NOT the filename! std::string LanguageID(); + // The loaded language is written right to left. + bool IsRTL() const; std::shared_ptr GetCategory(I18NCat category); diff --git a/Common/Render/Text/draw_text.cpp b/Common/Render/Text/draw_text.cpp index b4dcff9d33..3e4db69ce3 100644 --- a/Common/Render/Text/draw_text.cpp +++ b/Common/Render/Text/draw_text.cpp @@ -17,6 +17,7 @@ #include "Common/System/Display.h" #include "Common/GPU/thin3d.h" #include "Common/Data/Hash/Hash.h" +#include "Common/Data/Text/I18n.h" #include "Common/Data/Text/WrapText.h" #include "Common/Data/Encoding/Utf8.h" @@ -90,6 +91,33 @@ const std::string &TextDrawer::WrapString(const FontStyle &style, float scale, s return entry.wrapped; } +// The backends disagree on a line's base direction: GDI and DirectWrite always start left to right, the others +// go by the first strong character, which is wrong for a Hebrew sentence that starts with "PPSSPP". An explicit +// embedding (RLE ... PDF) means the same to all of them. +std::string_view TextDrawer::ApplyBaseDirection(std::string_view str, std::string *storage) { + if (!g_i18nrepo.IsRTL() || !UTF8StringHasRTL(str)) { + return str; + } + static constexpr std::string_view RLE = "\xE2\x80\xAB"; // U+202B + static constexpr std::string_view PDF = "\xE2\x80\xAC"; // U+202C + storage->clear(); + std::vector lines; + SplitString(str, '\n', lines); + for (size_t i = 0; i < lines.size(); i++) { + if (i > 0) { + storage->push_back('\n'); + } + if (UTF8StringHasRTL(lines[i])) { + storage->append(RLE); + storage->append(lines[i]); + storage->append(PDF); + } else { + storage->append(lines[i]); + } + } + return *storage; +} + float TextDrawer::CalculateDPIScale() const { if (ignoreGlobalDpi_) return dpiScale_; @@ -143,7 +171,8 @@ void TextDrawer::DrawString(DrawBuffer &target, const FontStyle &style, float sc TextureDesc desc{}; std::vector bitmapData; EnsureFont(style); - if (!DrawStringBitmap(style, bitmapData, *entry, bitmapFormat, str, align, emoji)) { + std::string bidiStorage; + if (!DrawStringBitmap(style, bitmapData, *entry, bitmapFormat, ApplyBaseDirection(str, &bidiStorage), align, emoji)) { // Nothing drawn. The entry without a texture records that fact. return; } @@ -199,7 +228,8 @@ void TextDrawer::MeasureString(const FontStyle &style, float scaleX, float scale if (inserted) { EnsureFont(style); float extW = 0.0f, extH = 0.0f; - MeasureStringInternal(style, str, &extW, &extH); + std::string bidiStorage; + MeasureStringInternal(style, ApplyBaseDirection(str, &bidiStorage), &extW, &extH); entry.width = extW; entry.height = extH; // Hm, use the old calculation? @@ -264,7 +294,9 @@ bool TextDrawer::DrawStringBitmapRect(const FontStyle &style, float scale, std:: const int wrap = align & (FLAG_WRAP_TEXT | FLAG_ELLIPSIZE_TEXT); EnsureFont(style); - return DrawStringBitmap(style, bitmapData, entry, texFormat, wrap ? std::string_view(WrapString(style, scale, str, bounds.w, wrap)) : str, align, fullColor); + std::string bidiStorage; + const std::string_view text = wrap ? std::string_view(WrapString(style, scale, str, bounds.w, wrap)) : str; + return DrawStringBitmap(style, bitmapData, entry, texFormat, ApplyBaseDirection(text, &bidiStorage), align, fullColor); } void TextDrawer::AddDropShadow(std::vector &bitmapData, TextStringEntry &entry, Draw::DataFormat srcFormat) const { diff --git a/Common/Render/Text/draw_text.h b/Common/Render/Text/draw_text.h index e337c5b8a6..d60178c968 100644 --- a/Common/Render/Text/draw_text.h +++ b/Common/Render/Text/draw_text.h @@ -114,6 +114,10 @@ protected: // Wraps and/or ellipsizes, caching the result. The reference stays valid until the next OncePerFrame. const std::string &WrapString(const FontStyle &style, float scale, std::string_view str, float maxWidth, int flags); + // When the UI language is right to left, puts each line containing right-to-left text in an RTL embedding, + // so every backend lays it out right to left, whatever its first character. Otherwise returns str as is. + static std::string_view ApplyBaseDirection(std::string_view str, std::string *storage); + // v has the 8-bit alpha. static u16 AlphaToPremul4444(u32 v) { v = (v >> 4) & 0x0F;