Merge pull request #22991 from hrydgard/rtl-text

Lay out right-to-left text consistently across text drawers, fix fa_IR/ar_AE strings
This commit is contained in:
Henrik Rydgård authored and GitHub committed 2026-11-08 14:33:53 +01:00
commit c04b85d035
10 files changed
+816 -747

No files matched your search

+18
View File
@@ -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) {
+2
View File
@@ -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.
+4
View File
@@ -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();
}
+2
View File
@@ -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<I18NCategory> GetCategory(I18NCat category);
+35 -3
View File
@@ -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<std::string_view> 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<uint8_t> 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<uint8_t> &bitmapData, TextStringEntry &entry, Draw::DataFormat srcFormat) const {
+4
View File
@@ -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;
+9 -1
View File
@@ -58,6 +58,15 @@ static bool ReorderBidi(std::string_view str, std::string *out) {
result.push_back('\n');
}
std::string_view line = lines[i];
FriBidiParType baseDir = FRIBIDI_PAR_ON;
// TextDrawer::ApplyBaseDirection wraps RTL lines in RLE ... PDF. Take the direction from that instead,
// so the marks don't count as a second level and send pure Arabic lines down the reordering path.
const std::string_view rle = "\xE2\x80\xAB";
const std::string_view pdf = "\xE2\x80\xAC";
if (line.size() >= rle.size() + pdf.size() && startsWith(line, rle) && endsWith(line, pdf)) {
line = line.substr(rle.size(), line.size() - rle.size() - pdf.size());
baseDir = FRIBIDI_PAR_RTL;
}
logical.resize(line.size() + 1);
FriBidiStrIndex len = fribidi_charset_to_unicode(FRIBIDI_CHAR_SET_UTF8, line.data(), (FriBidiStrIndex)line.size(), logical.data());
if (len <= 0) {
@@ -66,7 +75,6 @@ static bool ReorderBidi(std::string_view str, std::string *out) {
}
visual.resize(len + 1);
levels.resize(len);
FriBidiParType baseDir = FRIBIDI_PAR_ON;
if (!fribidi_log2vis(logical.data(), len, &baseDir, visual.data(), nullptr, nullptr, levels.data())) {
return false;
}
+2 -3
View File
@@ -304,9 +304,8 @@ NewLanguageScreen::NewLanguageScreen(std::string_view title) : ListPopupScreen(t
continue;
}
// We only support Arabic on platforms where we have support for the native text rendering
// APIs, as proper Arabic support is way too difficult to implement ourselves.
#if !(PPSSPP_PLATFORM(WINDOWS) || PPSSPP_PLATFORM(ANDROID))
// Arabic script needs a text drawer that does shaping (see TextDrawer::Create), the atlas font can't.
#if defined(__LIBRETRO__) || !(PPSSPP_PLATFORM(WINDOWS) || PPSSPP_PLATFORM(ANDROID) || PPSSPP_PLATFORM(MAC) || PPSSPP_PLATFORM(IOS) || defined(USE_SDL3_TTF))
if (tempLangs[i].name.find("ar_AE") != std::string::npos) {
continue;
}
+425 -425
View File
File diff suppressed because it is too large. Load diff
+315 -315
View File
File diff suppressed because it is too large. Load diff