From 352276a6be91b27ed02996711481a370bc2f9c63 Mon Sep 17 00:00:00 2001 From: Ghabry Date: Mon, 8 Jul 2024 15:01:41 +0200 Subject: [PATCH 1/5] Add experimental Bidi support --- CMakeLists.txt | 3 + src/font.cpp | 10 +-- src/font.h | 5 +- src/pending_message.cpp | 5 +- src/pending_message.h | 4 ++ src/text.cpp | 135 +++++++++++++++++++++++++++++++--------- src/text.h | 14 ++++- src/utils.cpp | 9 ++- src/utils.h | 6 +- src/window_help.cpp | 7 ++- src/window_message.cpp | 80 ++++++++++++++++-------- src/window_message.h | 4 +- 12 files changed, 209 insertions(+), 73 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index af487985dc..dcce691265 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -926,6 +926,9 @@ player_find_package(NAME fmt TARGET fmt::fmt VERSION 5.2 REQUIRED) find_package(Pixman REQUIRED) target_link_libraries(${PROJECT_NAME} PIXMAN::PIXMAN) +find_package(ICU COMPONENTS i18n uc data REQUIRED) +target_link_libraries(${PROJECT_NAME} ICU::i18n ICU::uc ICU::data) + # Always enable Wine registry support on non-Windows, but not for console ports if(NOT CMAKE_SYSTEM_NAME STREQUAL "Windows" AND NOT PLAYER_CONSOLE_PORT) diff --git a/src/font.cpp b/src/font.cpp index e51b23862a..df71144c04 100644 --- a/src/font.cpp +++ b/src/font.cpp @@ -26,6 +26,7 @@ #include "system.h" #include "game_system.h" #include "main_data.h" +#include "text.h" #ifdef HAVE_FREETYPE #ifndef __PS4__ @@ -157,7 +158,7 @@ namespace { GlyphRet vRenderShaped(char32_t glyph) const override; bool vCanShape() const override; #ifdef HAVE_HARFBUZZ - std::vector vShape(std::u32string_view txt) const override; + std::vector vShape(std::u32string_view txt, Text::Direction direction) const override; #endif void vApplyStyle(const Style& style) override; @@ -506,11 +507,12 @@ bool FTFont::vCanShape() const { } #ifdef HAVE_HARFBUZZ -std::vector FTFont::vShape(std::u32string_view txt) const { +std::vector FTFont::vShape(std::u32string_view txt, Text::Direction direction) const { hb_buffer_clear_contents(hb_buffer); hb_buffer_add_utf32(hb_buffer, reinterpret_cast(txt.data()), txt.size(), 0, txt.size()); hb_buffer_guess_segment_properties(hb_buffer); + hb_buffer_set_direction(hb_buffer, direction == Text::Direction::LTR ? HB_DIRECTION_LTR : HB_DIRECTION_RTL); hb_shape(hb_font, hb_buffer, nullptr, 0); @@ -899,10 +901,10 @@ bool Font::CanShape() const { return vCanShape(); } -std::vector Font::Shape(std::u32string_view text) const { +std::vector Font::Shape(std::u32string_view text, Text::Direction direction) const { assert(vCanShape()); - return vShape(text); + return vShape(text, direction); } void Font::SetFallbackFont(FontRef fallback_font) { diff --git a/src/font.h b/src/font.h index 85a0d81264..72209847c9 100644 --- a/src/font.h +++ b/src/font.h @@ -25,6 +25,7 @@ #include "memory_management.h" #include "rect.h" #include "string_view.h" +#include "text.h" #include #include @@ -185,7 +186,7 @@ class Font { * @param text Text to shape * @return Shaping information. See Font::ShapeRet */ - std::vector Shape(std::u32string_view text) const; + std::vector Shape(std::u32string_view text, Text::Direction direction) const; /** * Defines a fallback font that shall be used when a glyph is not found in the current font. @@ -251,7 +252,7 @@ class Font { virtual GlyphRet vRender(char32_t glyph) const = 0; virtual GlyphRet vRenderShaped(char32_t glyph) const { return vRender(glyph); }; virtual bool vCanShape() const { return false; } - virtual std::vector vShape(std::u32string_view) const { return {}; } + virtual std::vector vShape(std::u32string_view, Text::Direction) const { return {}; } virtual void vApplyStyle(const Style& style) { (void)style; }; protected: diff --git a/src/pending_message.cpp b/src/pending_message.cpp index aea1fba180..c02f6b6dee 100644 --- a/src/pending_message.cpp +++ b/src/pending_message.cpp @@ -23,6 +23,7 @@ #include "game_switches.h" #include #include "output.h" +#include "text.h" #include "utils.h" #include "player.h" #include "main_data.h" @@ -46,6 +47,7 @@ int PendingMessage::PushLineImpl(std::string msg) { RemoveControlChars(msg); msg = ApplyTextInsertingCommands(std::move(msg), Player::escape_char, command_inserter); texts.push_back(std::move(msg)); + runs.push_back(Text::Bidi(texts.back(), Text::Direction::LTR)); return texts.size(); } @@ -75,6 +77,7 @@ int PendingMessage::PushNumInput(int variable_id, int num_digits) { void PendingMessage::PushPageEnd() { assert(!HasChoices()); assert(!HasNumberInput()); + // FIXME: Runs if (texts.empty()) { texts.push_back(""); } @@ -132,7 +135,7 @@ std::string PendingMessage::ApplyTextInsertingCommands(std::string input, uint32 if (fn_res) { output.append(*fn_res); start_copy = iter; - } + } } if (start_copy == input.data()) { diff --git a/src/pending_message.h b/src/pending_message.h index 9e81a12eaa..3170cf1d16 100644 --- a/src/pending_message.h +++ b/src/pending_message.h @@ -17,6 +17,7 @@ #ifndef EP_PENDING_MESSAGE_H #define EP_PENDING_MESSAGE_H + #include #include #include @@ -24,6 +25,7 @@ #include #include #include "async_op.h" +#include "text.h" class PendingMessage { public: @@ -44,6 +46,7 @@ class PendingMessage { void SetChoiceContinuation(ChoiceContinuation f) { choice_continuation = std::move(f); } const std::vector& GetLines() const { return texts; } + const std::vector>& GetRuns() const { return runs; } bool IsActive() const { return NumLines() || HasNumberInput(); } int NumLines() const { return texts.size(); } @@ -78,6 +81,7 @@ class PendingMessage { CommandInserter command_inserter; ChoiceContinuation choice_continuation; std::vector texts; + std::vector> runs; int choice_start = -1; int choice_cancel_type = 5; int num_input_variable = 0; diff --git a/src/text.cpp b/src/text.cpp index 4fdc5d441b..3ce44c5abc 100644 --- a/src/text.cpp +++ b/src/text.cpp @@ -18,6 +18,7 @@ // Headers #include #include "cache.h" +#include "directory_tree.h" #include "output.h" #include "utils.h" #include "bitmap.h" @@ -27,6 +28,8 @@ #include #include +#include +#include Point Text::Draw(Bitmap& dest, int x, int y, const Font& font, const Bitmap& system, int color, char32_t glyph, bool is_exfont) { if (is_exfont) { @@ -84,50 +87,58 @@ Point Text::Draw(Bitmap& dest, const int x, const int y, const Font& font, const // This loops always renders a single char, color blends it and then puts // it onto the text_surface (including the drop shadow) - auto iter = text.data(); - const auto end = iter + text.size(); - if (font.CanShape()) { - // Collect all glyphs until ExFont or end of string and then shape and render - std::u32string text32; - while (iter != end) { - auto ret = Utils::TextNext(iter, end, 0); + auto runs = Bidi(text, RTL); - iter = ret.next; - if (EP_UNLIKELY(!ret)) { - continue; + // Collect all glyphs until ExFont or end of string and then shape and render + for (const auto& run: runs) { + if (runs.size() > 1) { + Output::Debug("{} {}", run.text, (int)run.direction); } + auto iter = run.text.data(); + const auto end = iter + run.text.size(); + std::u32string text32; + while (iter != end) { + auto ret = Utils::TextNext(iter, end, 0); + + iter = ret.next; + if (EP_UNLIKELY(!ret)) { + continue; + } - if (EP_UNLIKELY(Utils::IsControlCharacter(ret.ch))) { - next_glyph_pos += Draw(dest, ix + next_glyph_pos, iy, font, system, color, ret.ch, ret.is_exfont).x; - continue; - } + if (EP_UNLIKELY(Utils::IsControlCharacter(ret.ch))) { + next_glyph_pos += Draw(dest, ix + next_glyph_pos, iy, font, system, color, ret.ch, ret.is_exfont).x; + continue; + } - if (ret.is_exfont) { - if (!text32.empty()) { - auto shape_ret = font.Shape(text32); - text32.clear(); + if (ret.is_exfont) { + if (!text32.empty()) { + auto shape_ret = font.Shape(text32, run.direction); + text32.clear(); - for (const auto& ch: shape_ret) { - next_glyph_pos += font.Render(dest, ix + next_glyph_pos, iy, system, color, ch).x; + for (const auto& ch: shape_ret) { + next_glyph_pos += font.Render(dest, ix + next_glyph_pos, iy, system, color, ch).x; + } } + + next_glyph_pos += Draw(dest, ix + next_glyph_pos, iy, font, system, color, ret.ch, true).x; + continue; } - next_glyph_pos += Draw(dest, ix + next_glyph_pos, iy, font, system, color, ret.ch, true).x; - continue; + text32 += ret.ch; } - text32 += ret.ch; - } - - if (!text32.empty()) { - auto shape_ret = font.Shape(text32); + if (!text32.empty()) { + auto shape_ret = font.Shape(text32, run.direction); - for (const auto& ch: shape_ret) { - next_glyph_pos += font.Render(dest, ix + next_glyph_pos, iy, system, color, ch).x; + for (const auto& ch: shape_ret) { + next_glyph_pos += font.Render(dest, ix + next_glyph_pos, iy, system, color, ch).x; + } } } } else { + auto iter = text.data(); + const auto end = iter + text.size(); while (iter != end) { auto ret = Utils::TextNext(iter, end, 0); @@ -208,7 +219,7 @@ Rect Text::GetSize(const Font& font, std::string_view text) { if (ret.is_exfont) { if (!text32.empty()) { - auto shape_ret = font.Shape(text32); + auto shape_ret = font.Shape(text32, Direction::LTR); text32.clear(); for (const auto& ch: shape_ret) { @@ -228,7 +239,7 @@ Rect Text::GetSize(const Font& font, std::string_view text) { } if (!text32.empty()) { - auto shape_ret = font.Shape(text32); + auto shape_ret = font.Shape(text32, Direction::LTR); for (const auto& ch: shape_ret) { Rect size = font.GetSize(ch); @@ -267,3 +278,65 @@ Rect Text::GetSize(const Font& font, char32_t glyph, bool is_exfont) { return font.GetSize(glyph); } } + +#include +#include + +std::vector Text::Bidi(std::string_view text, Text::Direction text_direction) { + UErrorCode error_code = U_ZERO_ERROR; + + if (U_FAILURE(error_code) || text.empty()) { + return {}; + } + + auto text16 = Utils::DecodeUTF16(text); + + UBiDi* bidi = ubidi_openSized(text16.size(), 0, &error_code); + if (bidi == NULL) { + return {}; + } + + ubidi_setPara(bidi, text16.c_str(), text16.size(), + text_direction == 0 ? UBIDI_DEFAULT_LTR : UBIDI_DEFAULT_RTL, nullptr, &error_code); + + if (U_FAILURE(error_code)) { + ubidi_close(bidi); + return {}; + } + + UBiDiDirection direction = ubidi_getDirection(bidi); + std::vector runs; + + if (direction != UBIDI_MIXED) { + // unidirectional + runs.push_back({ToString(text), static_cast(direction)}); + } else { + // mixed-directional + int32_t count = ubidi_countRuns(bidi, &error_code); + + if (U_FAILURE(error_code)) { + ubidi_close(bidi); + return {}; + } + + int32_t start, length; + std::string subtext; + std::u32string subtext32; + + // iterate over directional runs + for (int32_t i = 0; i < count; ++i) { + direction = ubidi_getVisualRun(bidi, i, &start, &length); + + // FIXME: Doing UTF16-8-32 conversion, better would be a UTF16-32 conversion + subtext = Utils::EncodeUTF(std::u16string_view(text16.data() + start, length)); + //subtext32 = Utils::DecodeUTF32(subtext); + + runs.push_back({subtext, static_cast(direction)}); + } + + } + + ubidi_close(bidi); + + return runs; +} diff --git a/src/text.h b/src/text.h index 176a7f2d53..4e1c4150e1 100644 --- a/src/text.h +++ b/src/text.h @@ -40,6 +40,17 @@ namespace Text { AlignRight }; + enum Direction { + // Enum values equal to UBiDiDirection + LTR, + RTL + }; + + struct Run { + std::string text; + Direction direction; + }; + /** * Draws the text onto dest bitmap with given parameters. * @@ -86,7 +97,6 @@ namespace Text { */ Point Draw(Bitmap& dest, int x, int y, const Font& font, const Bitmap& system, int color, char32_t glyph, bool is_exfont); - /** * Draws the character onto dest bitmap with given parameters. Does not draw a shadow. * @@ -126,5 +136,7 @@ namespace Text { * @return Rect describing the rendered string boundary */ Rect GetSize(const Font& font, char32_t glyph, bool is_exfont); + + std::vector Bidi(std::string_view text, Direction text_direction); } #endif diff --git a/src/utils.cpp b/src/utils.cpp index 8600ddb359..0027ab1a4e 100644 --- a/src/utils.cpp +++ b/src/utils.cpp @@ -18,13 +18,16 @@ // Headers #include "utils.h" #include "compiler.h" +#include "string_view.h" #include #include #include #include #include -#include #include +#include +#include +#include #include namespace { @@ -234,7 +237,7 @@ std::u32string Utils::DecodeUTF32(std::string_view str) { return result; } -std::string Utils::EncodeUTF(const std::u16string& str) { +std::string Utils::EncodeUTF(std::u16string_view str) { std::string result; for (auto it = str.begin(), str_end = str.end(); it < str_end; ++it) { uint16_t wc1 = *it; @@ -277,7 +280,7 @@ std::string Utils::EncodeUTF(const std::u16string& str) { return result; } -std::string Utils::EncodeUTF(const std::u32string& str) { +std::string Utils::EncodeUTF(std::u32string_view str) { std::string result; for (const char32_t& wc : str) { if ((wc & 0xFFFFF800) == 0x00D800 || wc > 0x10FFFF) diff --git a/src/utils.h b/src/utils.h index dbd083bb02..a2593e7719 100644 --- a/src/utils.h +++ b/src/utils.h @@ -22,6 +22,8 @@ #include #include #include +#include +#include #include #include "string_view.h" #include "span.h" @@ -105,7 +107,7 @@ namespace Utils { * @param str string to convert. * @return the converted string. */ - std::string EncodeUTF(const std::u16string& str); + std::string EncodeUTF(std::u16string_view str); /** * Converts UTF-32 to UTF-8. @@ -113,7 +115,7 @@ namespace Utils { * @param str string to convert. * @return the converted string. */ - std::string EncodeUTF(const std::u32string& str); + std::string EncodeUTF(std::u32string_view str); struct UtfNextResult { const char* next = nullptr; diff --git a/src/window_help.cpp b/src/window_help.cpp index 4b5e7289d2..106b524421 100644 --- a/src/window_help.cpp +++ b/src/window_help.cpp @@ -54,7 +54,7 @@ void Window_Help::Clear() { } void Window_Help::AddText(std::string text, int color, Text::Alignment align, bool halfwidthspace) { - std::string::size_type pos = 0; + /*std::string::size_type pos = 0; std::string::size_type nextpos = 0; while (nextpos != std::string::npos) { nextpos = text.find(' ', pos); @@ -73,7 +73,10 @@ void Window_Help::AddText(std::string text, int color, Text::Alignment align, bo } pos = nextpos + 1; } - } + }*/ + + auto offset = contents->TextDraw(text_x_offset, 2, color, text, align); + text_x_offset += offset.x; } void Window_Help::SetAnimation(Window_Help::Animation animation) { diff --git a/src/window_message.cpp b/src/window_message.cpp index 243a1d65fa..4642a42548 100644 --- a/src/window_message.cpp +++ b/src/window_message.cpp @@ -133,7 +133,8 @@ Window_Message::~Window_Message() { } void Window_Message::StartMessageProcessing(PendingMessage pm) { - text.clear(); + text_runs.clear(); + active_run = 0; pending_message = std::move(pm); if (!IsVisible()) { @@ -145,27 +146,38 @@ void Window_Message::StartMessageProcessing(PendingMessage pm) { return; } - const auto& lines = pending_message.GetLines(); + //const auto& lines = pending_message.GetRuns(); + const auto& lines = pending_message.GetRuns(); int num_lines = 0; - auto append = [&](const std::string& line) { - bool force_page_break = (!line.empty() && line.back() == '\f'); + auto append = [&](const std::vector& runs) { + text_runs.insert(text_runs.end(), runs.begin(), runs.end()); + + bool force_page_break = false; + /*bool force_page_break = (!line.empty() && line.back() == '\f'); text.append(line, 0, line.size() - force_page_break); if (line.empty() || text.back() != '\n') { text.push_back('\n'); } + + //auto runs = Text::Bidi(line, Text::Direction::LTR); + */ + + text_runs.push_back({"\n", Text::Direction::LTR}); + ++num_lines; if (num_lines == GetMaxLinesPerPage() || force_page_break) { - text.push_back('\f'); + text_runs.back().text += '\f'; + //text.push_back('\f'); num_lines = 0; } }; - if (pending_message.IsWordWrapped()) { + /*if (pending_message.IsWordWrapped()) { for (const std::string& line : lines) { - /* TODO: don't take commands like \> \< into account when word-wrapping */ + // TODO: don't take commands like \> \< into account when word-wrapping Game_Message::WordWrap( line, width - 24, @@ -174,21 +186,21 @@ void Window_Message::StartMessageProcessing(PendingMessage pm) { } ); } - } else { - for (const std::string& line : lines) { - append(line); + } else {*/ + for (const auto& run : lines) { + append(run); } - } + //} - if (text.empty() || text.back() != '\f') { - text.push_back('\f'); + if (text_runs.empty() || text_runs.back().text.back() != '\f') { + text_runs.back().text += '\f'; } item_max = min(4, pending_message.GetNumChoices()); - text_index = text.data(); + text_index = text_runs[active_run].text.data(); - DebugLog("{}: MSG TEXT \n{}", text); + //DebugLog("{}: MSG TEXT \n{}", text_runs[active_run].text); disallow_next_message = true; msg_was_pushed_this_frame = true; @@ -336,10 +348,10 @@ void Window_Message::InsertNewPage() { ShowGoldWindow(); } else { // If first character is gold, the gold window appears immediately and animates open with the main window. - auto tret = Utils::TextNext(text_index, (text.data() + text.size()), Player::escape_char); + /*auto tret = Utils::TextNext(text_index, (text.data() + text.size()), Player::escape_char); if (tret && tret.is_escape && tret.ch == '$') { ShowGoldWindow(); - } + }*/ } } @@ -374,8 +386,8 @@ void Window_Message::InsertNewLine() { void Window_Message::FinishMessageProcessing() { DebugLog("{}: FINISH MSG"); - text.clear(); - text_index = text.data(); + text_runs.clear(); + text_index = nullptr; SetPause(false); kill_page = false; @@ -434,11 +446,12 @@ void Window_Message::Update() { disallow_next_message = true; return; } - if (!text.empty() && text_index == text.data()) { + + if (!text_runs.empty() && text_index == text_runs[active_run].text.data()) { auto open_frames = (!IsVisible() && !Game_Battle::IsBattleRunning()) ? message_animation_frames : 0; SetOpenAnimation(open_frames); DebugLog("{}: MSG START OPEN {}", open_frames); - + InsertNewPage(); } return; @@ -495,7 +508,10 @@ void Window_Message::UpdateMessage() { auto system = Cache::SystemOrBlack(); while (true) { - const auto* end = text.data() + text.size(); + const auto& run = text_runs[active_run]; + const auto* end = run.text.data() + run.text.size(); + + Output::Debug("Run {} {} {}", run.text, (int)(text_index - run.text.data()), (int)run.direction); if (wait_count > 0) { DebugLog("{}: MSG WAIT LOOP {}", wait_count); @@ -517,6 +533,12 @@ void Window_Message::UpdateMessage() { } if (text_index == end) { + ++active_run; + if (active_run < (int)text_runs.size()) { + text_index = text_runs[active_run].text.data(); + continue; + } + FinishMessageProcessing(); break; } @@ -682,15 +704,20 @@ void Window_Message::UpdateMessage() { text_index_shape = tret.next; auto chs = tret.ch; - if (text_index_shape == end || tret.is_exfont || tret.is_escape || Utils::IsControlCharacter(chs)) { + if (tret.is_exfont || tret.is_escape || Utils::IsControlCharacter(chs)) { text_index = text_prev_shape; break; } text32 += tret.ch; + + if (text_index_shape == end) { + text_index = text_prev_shape; + break; + } } - shape_ret = page_font->Shape(text32); + shape_ret = page_font->Shape(text32, text_runs[active_run].direction); continue; } else { if (!DrawGlyph(*page_font, *system, ch, false)) { @@ -869,13 +896,14 @@ void Window_Message::SetWaitForNonPrintable(int frames) { void Window_Message::SetWaitForCharacter(int width) { int frames = 0; if (!instant_speed && width > 0) { - bool is_last_for_page; + /*bool is_last_for_page; if (!shape_ret.empty()) { is_last_for_page = (shape_ret.size() == 1) && ( (text.data() + text.size() - text_index) <= 1 || (*text_index == '\n' && *(text_index + 1) == '\f')); } else { is_last_for_page = (text.data() + text.size() - text_index) <= 1 || (*text_index == '\n' && *(text_index + 1) == '\f'); - } + }*/ + bool is_last_for_page = false; if (is_last_for_page) { // RPG_RT always waits 2 frames for last character on the page. diff --git a/src/window_message.h b/src/window_message.h index 4c5b10325d..633b1cdd3e 100644 --- a/src/window_message.h +++ b/src/window_message.h @@ -150,9 +150,11 @@ class Window_Message: public Window_Selectable { /** Maximum number of lines per page */ int max_lines_per_page = 4; /** Index of the next char in text that will be output. */ + int active_run = 0; const char* text_index = nullptr; /** text message that will be displayed. */ - std::string text; + std::vector text_runs; + //std::string text; /** Text color. */ int text_color = 0; /** Current speed modifier. */ From 3eb811a4491b4326cc442bccb3e810b23b1b880b Mon Sep 17 00:00:00 2001 From: Ghabry Date: Wed, 31 Jul 2024 23:19:15 +0200 Subject: [PATCH 2/5] Proper Bidi RTL Rendering in Message boxes --- src/text.cpp | 44 +++++++++--- src/text.h | 3 + src/window_message.cpp | 155 +++++++++++++++++++++++++++++++++++++---- src/window_message.h | 2 + 4 files changed, 181 insertions(+), 23 deletions(-) diff --git a/src/text.cpp b/src/text.cpp index 3ce44c5abc..9dfbd65056 100644 --- a/src/text.cpp +++ b/src/text.cpp @@ -20,6 +20,7 @@ #include "cache.h" #include "directory_tree.h" #include "output.h" +#include "player.h" #include "utils.h" #include "bitmap.h" #include "font.h" @@ -29,8 +30,11 @@ #include #include #include + +#include #include + Point Text::Draw(Bitmap& dest, int x, int y, const Font& font, const Bitmap& system, int color, char32_t glyph, bool is_exfont) { if (is_exfont) { if (!font.IsStyleApplied()) { @@ -88,7 +92,7 @@ Point Text::Draw(Bitmap& dest, const int x, const int y, const Font& font, const // This loops always renders a single char, color blends it and then puts // it onto the text_surface (including the drop shadow) if (font.CanShape()) { - auto runs = Bidi(text, RTL); + auto runs = Bidi(text, LTR); // Collect all glyphs until ExFont or end of string and then shape and render for (const auto& run: runs) { @@ -279,8 +283,22 @@ Rect Text::GetSize(const Font& font, char32_t glyph, bool is_exfont) { } } -#include -#include +Text::Alignment Text::ScriptAlignment(std::string_view text, Text::Alignment align) { + if (text.empty() || !Player::IsRTL()) { + return align; + } + + auto bidi = Bidi(text, LTR); + + if (bidi.back().direction == LTR) { + return align; + } + + return + align == AlignLeft ? AlignRight : + align == AlignRight ? AlignLeft : + align; +} std::vector Text::Bidi(std::string_view text, Text::Direction text_direction) { UErrorCode error_code = U_ZERO_ERROR; @@ -321,19 +339,27 @@ std::vector Text::Bidi(std::string_view text, Text::Direction text_di int32_t start, length; std::string subtext; - std::u32string subtext32; + std::u16string subtext16; // iterate over directional runs for (int32_t i = 0; i < count; ++i) { direction = ubidi_getVisualRun(bidi, i, &start, &length); - // FIXME: Doing UTF16-8-32 conversion, better would be a UTF16-32 conversion - subtext = Utils::EncodeUTF(std::u16string_view(text16.data() + start, length)); - //subtext32 = Utils::DecodeUTF32(subtext); + subtext16 = std::u16string(text16.data() + start, length); - runs.push_back({subtext, static_cast(direction)}); - } + auto it = std::remove_if(subtext16.begin(), subtext16.end(), [](auto ch) { + // Filter out unicode bidi characters + return ch == u'\u200E' || ch == u'\u200F' || ch == u'\u061C' || + (ch >= u'\u202A' && ch <= u'\u202E') || + (ch >= u'\u2066' && ch <= u'\u2069'); + }); + subtext16.erase(it, subtext16.end()); + + if (!subtext16.empty()) { + runs.push_back({Utils::EncodeUTF(subtext16), static_cast(direction)}); + } + } } ubidi_close(bidi); diff --git a/src/text.h b/src/text.h index 4e1c4150e1..f0d41a3926 100644 --- a/src/text.h +++ b/src/text.h @@ -49,6 +49,7 @@ namespace Text { struct Run { std::string text; Direction direction; + int length = 0; }; /** @@ -137,6 +138,8 @@ namespace Text { */ Rect GetSize(const Font& font, char32_t glyph, bool is_exfont); + Alignment ScriptAlignment(std::string_view text, Text::Alignment align = Text::AlignLeft); + std::vector Bidi(std::string_view text, Direction text_direction); } #endif diff --git a/src/window_message.cpp b/src/window_message.cpp index 4642a42548..a92cbaf065 100644 --- a/src/window_message.cpp +++ b/src/window_message.cpp @@ -134,6 +134,7 @@ Window_Message::~Window_Message() { void Window_Message::StartMessageProcessing(PendingMessage pm) { text_runs.clear(); + line_direction.clear(); active_run = 0; pending_message = std::move(pm); @@ -151,7 +152,13 @@ void Window_Message::StartMessageProcessing(PendingMessage pm) { int num_lines = 0; auto append = [&](const std::vector& runs) { - text_runs.insert(text_runs.end(), runs.begin(), runs.end()); + if (runs.back().direction == Text::Direction::RTL) { + text_runs.insert(text_runs.end(), runs.rbegin(), runs.rend()); + line_direction.push_back(Text::Direction::RTL); + } else { + text_runs.insert(text_runs.end(), runs.begin(), runs.end()); + line_direction.push_back(Text::Direction::LTR); + } bool force_page_break = false; /*bool force_page_break = (!line.empty() && line.back() == '\f'); @@ -196,6 +203,70 @@ void Window_Message::StartMessageProcessing(PendingMessage pm) { text_runs.back().text += '\f'; } + for (auto& run: text_runs) { + std::u32string line32; + const auto* text_index = run.text.data(); + const auto* end = run.text.data() + run.text.size(); + + int line_width = 0; + while (text_index != end) { + auto tret = Utils::TextNext(text_index, end, Player::escape_char); + text_index = tret.next; + + if (EP_UNLIKELY(!tret)) { + continue; + } + + const auto ch = tret.ch; + + if (Utils::IsControlCharacter(ch)) { + // control characters not handled + continue; + } + + if (tret.is_exfont) { + // exfont processed later + line32 += '$'; + } + + if (tret.is_escape && ch != Player::escape_char) { + if (!line32.empty()) { + line_width += Text::GetSize(*Font::Default(), Utils::EncodeUTF(line32)).width; + line32.clear(); + } + + // Special message codes + switch (ch) { + case 'c': + case 'C': + { + // Color + text_index = Game_Message::ParseColor(text_index, end, Player::escape_char, true).next; + } + break; + case 's': + case 'S': + { + // Color + text_index = Game_Message::ParseColor(text_index, end, Player::escape_char, true).next; + } + break; + default: + break; + } + continue; + } + + line32 += static_cast(ch); + } + + if (!line32.empty()) { + line_width += Text::GetSize(*Font::Default(), Utils::EncodeUTF(line32)).width; + } + + run.length = line_width; + } + item_max = min(4, pending_message.GetNumChoices()); text_index = text_runs[active_run].text.data(); @@ -329,7 +400,11 @@ void Window_Message::InsertNewPage() { DrawFace(Main_Data::game_system->GetMessageFaceName(), Main_Data::game_system->GetMessageFaceIndex(), 248, TopMargin, Main_Data::game_system->IsMessageFaceFlipped()); } } else { - contents_x = 0; + if (line_direction[line_count] == Text::RTL) { + contents_x = 304; + } else { + contents_x = 0; + } } if (pending_message.GetChoiceStartLine() == 0 && pending_message.HasChoices()) { @@ -357,15 +432,20 @@ void Window_Message::InsertNewPage() { void Window_Message::InsertNewLine() { DebugLog("{}: MSG NEW LINE"); + + contents_y += 16; + ++line_count; + if (IsFaceEnabled() && !Main_Data::game_system->IsMessageFaceRightPosition()) { contents_x = LeftMargin + FaceSize + RightFaceMargin; } else { - contents_x = 0; + if (line_count < (int)line_direction.size() && line_direction[line_count] == Text::RTL) { + contents_x = 304; + } else { + contents_x = 0; + } } - contents_y += 16; - ++line_count; - if (pending_message.HasChoices() && line_count >= pending_message.GetChoiceStartLine()) { unsigned choice_index = line_count - pending_message.GetChoiceStartLine(); if (pending_message.GetChoiceResetColor()) { @@ -520,11 +600,20 @@ void Window_Message::UpdateMessage() { } if (!shape_ret.empty()) { - if (!DrawGlyph(*page_font, *system, shape_ret[0])) { - continue; + if (run.direction == Text::Direction::LTR) { + if (!DrawGlyph(*page_font, *system, shape_ret.front())) { + continue; + } + + shape_ret.erase(shape_ret.begin()); + } else { + if (!DrawGlyph(*page_font, *system, shape_ret.back())) { + continue; + } + + shape_ret.erase(shape_ret.end()); } - shape_ret.erase(shape_ret.begin()); continue; } @@ -533,8 +622,30 @@ void Window_Message::UpdateMessage() { } if (text_index == end) { + auto prev_run = text_runs[active_run]; + ++active_run; if (active_run < (int)text_runs.size()) { + auto cur_run = text_runs[active_run]; + + if (line_direction[line_count] == Text::LTR) { + + } else { + if (prev_run.direction == Text::LTR) { + if (cur_run.direction == Text::LTR) { + contents_x -= (prev_run.length + cur_run.length); + } else { // RTL + contents_x -= prev_run.length; + } + } else { // RTL + if (cur_run.direction == Text::LTR) { + contents_x -= cur_run.length; + } else { // RTL + // no-op + } + } + } + text_index = text_runs[active_run].text.data(); continue; } @@ -628,7 +739,11 @@ void Window_Message::UpdateMessage() { break; case '_': // Insert half size space - contents_x += Text::GetSize(*page_font, " ").width / 2; + if (run.direction == Text::Direction::LTR) { + contents_x += Text::GetSize(*page_font, " ").width / 2; + } else { + contents_x -= Text::GetSize(*page_font, " ").width / 2; + } DebugLogText("{}: MSG HalfWait \\_"); SetWaitForCharacter(1); break; @@ -712,7 +827,7 @@ void Window_Message::UpdateMessage() { text32 += tret.ch; if (text_index_shape == end) { - text_index = text_prev_shape; + text_index = end; break; } } @@ -790,13 +905,25 @@ bool Window_Message::DrawGlyph(Font& font, const Bitmap& system, const Font::Sha } } - auto rect = font.Render(*contents, contents_x, contents_y, system, text_color, shape); + auto& run = text_runs[active_run]; + + int glyph_width; + + if (run.direction == Text::Direction::LTR) { + auto rect = font.Render(*contents, contents_x, contents_y, system, text_color, shape); + glyph_width = rect.x; + contents_x += glyph_width; + } else { + contents_x -= shape.advance.x; + auto rect = font.Render(*contents, contents_x, contents_y, system, text_color, shape); + glyph_width = rect.x; + } - int glyph_width = rect.x; - contents_x += glyph_width; int width = get_width(glyph_width); SetWaitForCharacter(width); + SetWait(3); + return true; } diff --git a/src/window_message.h b/src/window_message.h index 633b1cdd3e..8abea7d72b 100644 --- a/src/window_message.h +++ b/src/window_message.h @@ -20,6 +20,7 @@ // Headers #include +#include "text.h" #include "window_gold.h" #include "window_numberinput.h" #include "window_selectable.h" @@ -154,6 +155,7 @@ class Window_Message: public Window_Selectable { const char* text_index = nullptr; /** text message that will be displayed. */ std::vector text_runs; + std::vector line_direction; //std::string text; /** Text color. */ int text_color = 0; From c95e60e3e5db4db071694811bffe7b2a86604f22 Mon Sep 17 00:00:00 2001 From: Ghabry Date: Wed, 31 Jul 2024 23:21:03 +0200 Subject: [PATCH 3/5] Add Bidi handling to some text rendering Currently partially broken --- src/player.cpp | 9 +++++++++ src/player.h | 5 +++++ src/text.h | 1 + src/window_base.cpp | 5 +++-- src/window_base.h | 2 +- src/window_command.cpp | 7 +++++-- src/window_equip.cpp | 2 +- src/window_help.cpp | 7 ++++++- src/window_item.cpp | 2 +- src/window_shopbuy.cpp | 2 +- src/window_shopnumber.cpp | 2 +- 11 files changed, 34 insertions(+), 10 deletions(-) diff --git a/src/player.cpp b/src/player.cpp index bb4799c4d7..d0e6076278 100644 --- a/src/player.cpp +++ b/src/player.cpp @@ -75,6 +75,7 @@ #include "scene_settings.h" #include "scene_title.h" #include "instrumentation.h" +#include "translation.h" #include "transition.h" #include #include @@ -1628,6 +1629,14 @@ bool Player::IsCP1251() { return (encoding == "ibm-5347_P100-1998" || encoding == "windows-1251" || encoding == "1251"); } +bool Player::IsRTL() { + if (Tr::HasActiveTranslation() && !Tr::GetCurrentLanguageCode().empty()) { + return Tr::GetCurrentLanguageCode() == "ar_AR" || Tr::GetCurrentLanguageCode() == "he_IL"; + } + + return false; +} + int Player::EngineVersion() { if (IsRPG2k3()) return 2003; if (IsRPG2k()) return 2000; diff --git a/src/player.h b/src/player.h index ece1b465e1..839c18efba 100644 --- a/src/player.h +++ b/src/player.h @@ -273,6 +273,11 @@ namespace Player { */ bool IsCP1251(); + /** + * @return true when the active language is right-to-left like Arabic or Hebrew. + */ + bool IsRTL(); + /** @return true when engine is 2k3 or the 2k3-commands patch is enabled */ bool IsRPG2k3Commands(); diff --git a/src/text.h b/src/text.h index f0d41a3926..67e88e9766 100644 --- a/src/text.h +++ b/src/text.h @@ -25,6 +25,7 @@ #include "color.h" #include "string_view.h" #include +#include class Font; class Bitmap; diff --git a/src/window_base.cpp b/src/window_base.cpp index 5c0c436821..5dd7977baa 100644 --- a/src/window_base.cpp +++ b/src/window_base.cpp @@ -18,6 +18,7 @@ // Headers #include #include +#include "text.h" #include "window_base.h" #include "cache.h" #include @@ -278,10 +279,10 @@ void Window_Base::DrawEquipmentType(const Game_Actor& actor, int cx, int cy, int contents->TextDraw(cx, cy, 1, name); } -void Window_Base::DrawItemName(const lcf::rpg::Item& item, int cx, int cy, bool enabled) const { +void Window_Base::DrawItemName(const lcf::rpg::Item& item, const Rect& rect, bool enabled) const { int color = enabled ? Font::ColorDefault : Font::ColorDisabled; - contents->TextDraw(cx, cy, color, item.name); + contents->TextDraw(rect, color, item.name, Text::ScriptAlignment(item.name)); } void Window_Base::DrawSkillName(const lcf::rpg::Skill& skill, int cx, int cy, bool enabled) const { diff --git a/src/window_base.h b/src/window_base.h index 68afdd94bc..bffc1f5b32 100644 --- a/src/window_base.h +++ b/src/window_base.h @@ -64,7 +64,7 @@ class Window_Base : public Window { void DrawActorSp(const Game_Battler& actor, int cx, int cy, int digits, bool draw_max = true) const; void DrawActorParameter(const Game_Battler& actor, int cx, int cy, int type) const; void DrawEquipmentType(const Game_Actor& actor, int cx, int cy, int type) const; - void DrawItemName(const lcf::rpg::Item& item, int cx, int cy, bool enabled = true) const; + void DrawItemName(const lcf::rpg::Item& item, const Rect& rect, bool enabled = true) const; void DrawSkillName(const lcf::rpg::Skill& skill, int cx, int cy, bool enabled = true) const; void DrawCurrencyValue(int money, int cx, int cy) const; void DrawGauge(const Game_Battler& actor, int cx, int cy, int alpha = 255) const; diff --git a/src/window_command.cpp b/src/window_command.cpp index ea15af4f97..941d12cc09 100644 --- a/src/window_command.cpp +++ b/src/window_command.cpp @@ -19,6 +19,7 @@ #include "window_command.h" #include "color.h" #include "bitmap.h" +#include "text.h" #include "util_macro.h" static int CalculateWidth(const std::vector& commands, int width) { @@ -47,8 +48,10 @@ void Window_Command::Refresh() { } void Window_Command::DrawItem(int index, Font::SystemColor color) { - contents->ClearRect(Rect(0, menu_item_height * index, contents->GetWidth() - 0, menu_item_height)); - contents->TextDraw(0, menu_item_height * index + menu_item_height / 8, color, commands[index]); + Rect rect = Rect(0, menu_item_height * index, contents->GetWidth() - 0, menu_item_height); + contents->ClearRect(rect); + rect.y = menu_item_height * index + menu_item_height / 8; + contents->TextDraw(rect, color, commands[index], Text::ScriptAlignment(commands[index])); } void Window_Command::DisableItem(int i) { diff --git a/src/window_equip.cpp b/src/window_equip.cpp index eb47ceca93..242ca91c66 100644 --- a/src/window_equip.cpp +++ b/src/window_equip.cpp @@ -53,7 +53,7 @@ void Window_Equip::Refresh() { DrawEquipmentType(actor, 0, (12 + 4) * i + 2, i); if (data[i] > 0) { // Equipment and items are guaranteed to be valid - DrawItemName(*lcf::ReaderUtil::GetElement(lcf::Data::items, data[i]), 60, (12 + 4) * i + 2); + //DrawItemName(*lcf::ReaderUtil::GetElement(lcf::Data::items, data[i]), 60, (12 + 4) * i + 2); } } } diff --git a/src/window_help.cpp b/src/window_help.cpp index 106b524421..80b10ceb5a 100644 --- a/src/window_help.cpp +++ b/src/window_help.cpp @@ -19,6 +19,7 @@ #include "window_help.h" #include "bitmap.h" #include "font.h" +#include "text.h" Window_Help::Window_Help(int ix, int iy, int iwidth, int iheight, Drawable::Flags flags) : Window_Base(ix, iy, iwidth, iheight, flags), @@ -75,7 +76,11 @@ void Window_Help::AddText(std::string text, int color, Text::Alignment align, bo } }*/ - auto offset = contents->TextDraw(text_x_offset, 2, color, text, align); + Rect rect = GetContents()->GetRect(); + rect.x = text_x_offset; + rect.y = 2; + + auto offset = contents->TextDraw(rect, color, text, Text::ScriptAlignment(text)); text_x_offset += offset.x; } diff --git a/src/window_item.cpp b/src/window_item.cpp index 592452a2e7..4c66e5cf65 100644 --- a/src/window_item.cpp +++ b/src/window_item.cpp @@ -122,7 +122,7 @@ void Window_Item::DrawItem(int index) { } bool enabled = CheckEnable(item_id); - DrawItemName(*item, rect.x, rect.y, enabled); + DrawItemName(*item, rect, enabled); Font::SystemColor color = enabled ? Font::ColorDefault : Font::ColorDisabled; contents->TextDraw(rect.x + rect.width - 24, rect.y, color, fmt::format("{}{:3d}", lcf::rpg::Terms::TermOrDefault(lcf::Data::terms.easyrpg_item_number_separator, ":"), number)); diff --git a/src/window_shopbuy.cpp b/src/window_shopbuy.cpp index 715a0bce83..1931dacb60 100644 --- a/src/window_shopbuy.cpp +++ b/src/window_shopbuy.cpp @@ -72,7 +72,7 @@ void Window_ShopBuy::DrawItem(int index) { } bool enabled = CheckEnable(item_id); - DrawItemName(*item, rect.x, rect.y, enabled); + DrawItemName(*item, rect, enabled); Font::SystemColor color = enabled ? Font::ColorDefault : Font::ColorDisabled; contents->TextDraw(rect.width, rect.y, color, std::to_string(item->price), Text::AlignRight); diff --git a/src/window_shopnumber.cpp b/src/window_shopnumber.cpp index e9fb264e44..98b22a783c 100644 --- a/src/window_shopnumber.cpp +++ b/src/window_shopnumber.cpp @@ -45,7 +45,7 @@ void Window_ShopNumber::Refresh() { int y = 34; // (Shop) items are guaranteed to be valid - DrawItemName(*lcf::ReaderUtil::GetElement(lcf::Data::items, item_id), 0, y); + //DrawItemName(*lcf::ReaderUtil::GetElement(lcf::Data::items, item_id), 0, y); std::stringstream ss; ss << number; From b855268357fd7207b971880025a3e2d6cb3cfda7 Mon Sep 17 00:00:00 2001 From: Ghabry Date: Wed, 31 Jul 2024 23:21:17 +0200 Subject: [PATCH 4/5] RTL Mirror the Menu scene --- src/scene_menu.cpp | 3 +++ src/window.cpp | 6 ++++++ src/window.h | 2 ++ 3 files changed, 11 insertions(+) diff --git a/src/scene_menu.cpp b/src/scene_menu.cpp index 98c3b81128..080ab90376 100644 --- a/src/scene_menu.cpp +++ b/src/scene_menu.cpp @@ -50,10 +50,12 @@ void Scene_Menu::Start() { // Gold Window gold_window.reset(new Window_Gold(Player::menu_offset_x, (Player::screen_height - gold_window_height - Player::menu_offset_y), gold_window_width, gold_window_height)); + gold_window->ApplyRtlMirror(); // Status Window menustatus_window.reset(new Window_MenuStatus(Player::menu_offset_x + menu_command_width, Player::menu_offset_y, (MENU_WIDTH - menu_command_width), MENU_HEIGHT)); menustatus_window->SetActive(false); + menustatus_window->ApplyRtlMirror(); } void Scene_Menu::Continue(SceneType /* prev_scene */) { @@ -161,6 +163,7 @@ void Scene_Menu::CreateCommandWindow() { command_window.reset(new Window_Command(options, menu_command_width)); command_window->SetX(Player::menu_offset_x); command_window->SetY(Player::menu_offset_y); + command_window->ApplyRtlMirror(); command_window->SetIndex(menu_index); // Disable items diff --git a/src/window.cpp b/src/window.cpp index dac94dbe1a..c447c6985e 100644 --- a/src/window.cpp +++ b/src/window.cpp @@ -74,6 +74,12 @@ void Window::SetBackgroundPreserveTransparentColor(bool preserve) { bg_preserve_transparent_color = preserve; } +void Window::ApplyRtlMirror() { + if (Player::IsRTL()) { + SetX(Player::menu_offset_x + Player::screen_width - GetX() - GetWidth()); + } +} + void Window::Draw(Bitmap& dst) { if (width <= 0 || height <= 0) return; if (x < -width || x > dst.GetWidth() || y < -height || y > dst.GetHeight()) return; diff --git a/src/window.h b/src/window.h index 7871b853a1..949f4759c9 100644 --- a/src/window.h +++ b/src/window.h @@ -100,6 +100,8 @@ class Window : public Drawable { bool IsClosing() const; bool IsOpeningOrClosing() const; + void ApplyRtlMirror(); + protected: virtual bool IsSystemGraphicUpdateAllowed() const; From f453517737436decee965fb750fede367bc7f9d4 Mon Sep 17 00:00:00 2001 From: Ghabry Date: Sat, 24 Aug 2024 19:15:54 +0200 Subject: [PATCH 5/5] Bidi: Fix handling of empty lines and deleting of the last run --- src/window_message.cpp | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/src/window_message.cpp b/src/window_message.cpp index a92cbaf065..91ee2722a7 100644 --- a/src/window_message.cpp +++ b/src/window_message.cpp @@ -152,12 +152,17 @@ void Window_Message::StartMessageProcessing(PendingMessage pm) { int num_lines = 0; auto append = [&](const std::vector& runs) { - if (runs.back().direction == Text::Direction::RTL) { - text_runs.insert(text_runs.end(), runs.rbegin(), runs.rend()); - line_direction.push_back(Text::Direction::RTL); - } else { - text_runs.insert(text_runs.end(), runs.begin(), runs.end()); + if (runs.empty()) { line_direction.push_back(Text::Direction::LTR); + } else { + if (runs.back().direction == Text::Direction::RTL) { + text_runs.insert(text_runs.end(), runs.rbegin(), runs.rend()); + line_direction.push_back(Text::Direction::RTL); + } + else { + text_runs.insert(text_runs.end(), runs.begin(), runs.end()); + line_direction.push_back(Text::Direction::LTR); + } } bool force_page_break = false; @@ -199,7 +204,9 @@ void Window_Message::StartMessageProcessing(PendingMessage pm) { } //} - if (text_runs.empty() || text_runs.back().text.back() != '\f') { + if (text_runs.empty()) { + text_runs.push_back({"\f", Text::Direction::LTR}); + } else if (text_runs.back().text.back() != '\f') { text_runs.back().text += '\f'; } @@ -611,7 +618,7 @@ void Window_Message::UpdateMessage() { continue; } - shape_ret.erase(shape_ret.end()); + shape_ret.pop_back(); } continue;