mirror of
https://github.com/hrydgard/ppsspp.git
synced 2026-11-13 09:39:33 +01:00
TextDrawer: Lay out right-to-left text the same way on every backend
GDI and DirectWrite started every line left to right, while the SDL, Cocoa and Android drawers took the direction from the first strong character. So a Hebrew line mixing in Latin words came out in the wrong order on Windows, and on the others when it started with "PPSSPP". When the UI language is right to left, each line containing RTL text now goes to the backend inside an explicit RTL embedding (RLE ... PDF), which all of them honor. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
fc11ace620
commit
ffe3d7bee5
6 files changed
+65
-3
No files matched your search
@@ -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) {
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in new issue
Block a user