Fix RTL text rendering.

This commit is contained in:
Martín Lucas Golini
2025-11-11 01:43:43 -03:00
parent 2008b57033
commit 7fc992ef2a
8 changed files with 221 additions and 109 deletions
Binary file not shown.
@@ -0,0 +1 @@
الإنتاج سيتضاعف! الإيجار سيختفي! النصر للشعب! ⚒️🚩
@@ -0,0 +1,7 @@
Hello!
This is a test sentence mixing Latin script with Arabic (like مرحبا، يا صديقي! which means "Hello, my friend!")
and Bengali (আমার নাম রহস্যময়। which means "My name is mysterious."), plus some emojis 😊🚀🌟.
Now, let's embed RTL inside LTR: The word "English" followed by العربية (Arabic) and then back to "text".
In Bengali: বাংলা ভাষা is beautiful, and mixing with Arabic: বাংলা مع العربية.
Finally, numbers 123 in Arabic context: ١٢٣، and emojis in RTL: 😊 مرحبا 😊.
Punctuation test: (Hello? مرحبا!) [End: 😎].
+6 -2
View File
@@ -12,7 +12,7 @@ class FontTrueType;
class EE_API TextShapeRun {
public:
TextShapeRun( String::View str, FontTrueType* font, Uint32 characterSize, Uint32 style,
Float outlineThickness );
Float outlineThickness, bool isRTL );
String::View curRun() const;
@@ -29,16 +29,20 @@ class EE_API TextShapeRun {
protected:
void findNextEnd();
inline bool checkRun( std::size_t idx, std::size_t pos );
String::View mString;
std::size_t mIndex{ 0 };
std::size_t mLen{ 0 };
Font* mFont{ nullptr };
Font* mLFont{ nullptr };
Uint32 mCharacterSize;
Uint32 mStyle;
Float mOutlineThickness;
Font* mCurFont{ nullptr };
Font* mStartFont{ nullptr };
bool mIsNewLine{ false };
bool mIsNewLine : 1 { false };
bool mIsRTL : 1 { false };
};
} // namespace EE::Graphics
+83 -59
View File
@@ -24,8 +24,8 @@ struct TextSegment {
};
// Split string into segments with uniform text properties
static void segmentString( String::View input,
std::function<bool( const TextSegment& segment )> cb ) {
static void segmentString( String::View input, std::function<bool( const TextSegment& segment )> cb,
const std::function<void()>& newParagraphCb ) {
const SBCodepointSequence codepointSequence{
SBStringEncodingUTF32, static_cast<const void*>( input.data() ), input.size() };
auto* const scriptLocator = SBScriptLocatorCreate();
@@ -39,6 +39,14 @@ static void segmentString( String::View input,
std::numeric_limits<SBUInteger>::max(), &paragraphLength,
&separatorLength );
// If the paragraph contains characters besides the separator,
// split the separator off into its own paragraph in the next iteration
// We do this to ensure line breaks are inserted into segments last
// after all character runs on the same line have already been inserted
// This allows us to draw our segments in left-to-right top-to-bottom order
if ( separatorLength < paragraphLength )
paragraphLength -= separatorLength;
auto* const paragraph = SBAlgorithmCreateParagraph( algorithm, paragraphOffset,
paragraphLength, SBLevelDefaultLTR );
auto* const line = SBParagraphCreateLine( paragraph, paragraphOffset, paragraphLength );
@@ -72,6 +80,8 @@ static void segmentString( String::View input,
SBParagraphRelease( paragraph );
paragraphOffset += paragraphLength;
newParagraphCb();
}
SBAlgorithmRelease( algorithm );
@@ -87,75 +97,83 @@ static bool shapeAndRun( const String& string, FontTrueType* font, Uint32 charac
Uint32 style, Float outlineThickness,
const std::function<bool( hb_glyph_info_t*, hb_glyph_position_t*, Uint32,
const hb_segment_properties_t&,
const TextSegment&, TextShapeRun& )>& cb ) {
const TextSegment&, TextShapeRun& )>& cb,
const std::function<void()>& newParagraphCb ) {
String::View input = string.view();
hb_buffer_t* hbBuffer = hb_buffer_create();
bool completeRun = true;
segmentString( input, [&]( const TextSegment& segment ) {
TextShapeRun run( input.substr( segment.offset, segment.length ), font, characterSize,
style, outlineThickness );
segmentString(
input,
[&]( const TextSegment& segment ) {
TextShapeRun run( input.substr( segment.offset, segment.length ), font, characterSize,
style, outlineThickness, segment.direction == HB_DIRECTION_RTL );
while ( run.hasNext() ) {
FontTrueType* font = run.font();
if ( font == nullptr ) { // empty line
run.next();
continue;
}
String::View curRun( run.curRun() );
font->setCurrentSize( characterSize );
hb_buffer_reset( hbBuffer );
hb_buffer_set_cluster_level( hbBuffer, HB_BUFFER_CLUSTER_LEVEL_MONOTONE_CHARACTERS );
hb_buffer_add_utf32( hbBuffer, (Uint32*)curRun.data(), curRun.size(), 0,
curRun.size() );
while ( run.hasNext() ) {
FontTrueType* font = run.font();
if ( font == nullptr ) { // empty line
run.next();
continue;
}
String::View curRun( run.curRun() );
font->setCurrentSize( characterSize );
hb_buffer_reset( hbBuffer );
hb_buffer_set_cluster_level( hbBuffer,
HB_BUFFER_CLUSTER_LEVEL_MONOTONE_CHARACTERS );
hb_buffer_add_utf32( hbBuffer, (Uint32*)curRun.data(), curRun.size(), 0,
curRun.size() );
hb_buffer_set_direction( hbBuffer, segment.direction );
hb_buffer_set_script( hbBuffer, segment.script );
hb_buffer_guess_segment_properties( hbBuffer );
hb_segment_properties_t props;
hb_buffer_get_segment_properties( hbBuffer, &props );
std::uint32_t featuresEnabled = !isSimpleScript( segment.script ) ? 1 : 0;
hb_buffer_set_direction( hbBuffer, segment.direction );
hb_buffer_set_script( hbBuffer, segment.script );
hb_buffer_guess_segment_properties( hbBuffer );
hb_segment_properties_t props;
hb_buffer_get_segment_properties( hbBuffer, &props );
std::uint32_t featuresEnabled = !isSimpleScript( segment.script ) ? 1 : 0;
// We use our own kerning algo
const hb_feature_t features[] = {
hb_feature_t{ HB_TAG( 'k', 'e', 'r', 'n' ), featuresEnabled,
HB_FEATURE_GLOBAL_START, HB_FEATURE_GLOBAL_END },
hb_feature_t{ HB_TAG( 'l', 'i', 'g', 'a' ), featuresEnabled,
HB_FEATURE_GLOBAL_START, HB_FEATURE_GLOBAL_END },
hb_feature_t{ HB_TAG( 'c', 'l', 'i', 'g' ), featuresEnabled,
HB_FEATURE_GLOBAL_START, HB_FEATURE_GLOBAL_END },
hb_feature_t{ HB_TAG( 'd', 'l', 'i', 'g' ), featuresEnabled,
HB_FEATURE_GLOBAL_START, HB_FEATURE_GLOBAL_END },
};
// We use our own kerning algo
const hb_feature_t features[] = {
hb_feature_t{ HB_TAG( 'k', 'e', 'r', 'n' ), featuresEnabled,
HB_FEATURE_GLOBAL_START, HB_FEATURE_GLOBAL_END },
hb_feature_t{ HB_TAG( 'l', 'i', 'g', 'a' ), featuresEnabled,
HB_FEATURE_GLOBAL_START, HB_FEATURE_GLOBAL_END },
hb_feature_t{ HB_TAG( 'c', 'l', 'i', 'g' ), featuresEnabled,
HB_FEATURE_GLOBAL_START, HB_FEATURE_GLOBAL_END },
hb_feature_t{ HB_TAG( 'd', 'l', 'i', 'g' ), featuresEnabled,
HB_FEATURE_GLOBAL_START, HB_FEATURE_GLOBAL_END },
};
// whitelist cross-platforms shapers only
static const char* shaper_list[] = { "ot", "graphite2", "fallback", nullptr };
// whitelist cross-platforms shapers only
static const char* shaper_list[] = { "ot", "graphite2", "fallback", nullptr };
if ( !font || !font->hb() ) {
eeASSERT( font && font->hb() );
completeRun = false;
break;
if ( !font || !font->hb() ) {
eeASSERT( font && font->hb() );
completeRun = false;
break;
}
hb_shape_full( static_cast<hb_font_t*>( font->hb() ), hbBuffer, features,
eeARRAY_SIZE( features ), shaper_list );
// from the shaped text we get the glyphs and positions
unsigned int glyphCount;
hb_glyph_info_t* glyphInfo = hb_buffer_get_glyph_infos( hbBuffer, &glyphCount );
hb_glyph_position_t* glyphPos =
hb_buffer_get_glyph_positions( hbBuffer, &glyphCount );
if ( cb( glyphInfo, glyphPos, glyphCount, props, segment, run ) )
run.next();
else {
completeRun = false;
return false;
break;
}
}
hb_shape_full( static_cast<hb_font_t*>( font->hb() ), hbBuffer, features,
eeARRAY_SIZE( features ), shaper_list );
return true;
},
[] {
// from the shaped text we get the glyphs and positions
unsigned int glyphCount;
hb_glyph_info_t* glyphInfo = hb_buffer_get_glyph_infos( hbBuffer, &glyphCount );
hb_glyph_position_t* glyphPos = hb_buffer_get_glyph_positions( hbBuffer, &glyphCount );
if ( cb( glyphInfo, glyphPos, glyphCount, props, segment, run ) )
run.next();
else {
completeRun = false;
return false;
break;
}
}
return true;
} );
} );
hb_buffer_destroy( hbBuffer );
return completeRun;
@@ -314,6 +332,12 @@ TextLayout TextLayouter::layout( const StringType& string, Font* font, const Uin
pen.y += vspace;
}
return true;
},
[&] {
result.linesWidth.push_back( std::ceil( pen.x ) );
maxWidth = eemax( maxWidth, result.linesWidth[result.linesWidth.size() - 1] );
pen.x = 0;
pen.y += vspace;
} );
} else
#endif
+119 -44
View File
@@ -10,30 +10,43 @@
namespace EE::Graphics {
TextShapeRun::TextShapeRun( String::View str, FontTrueType* font, Uint32 characterSize,
Uint32 style, Float outlineThickness ) :
Uint32 style, Float outlineThickness, bool isRTL ) :
mString( str ),
mFont( font ),
mCharacterSize( characterSize ),
mStyle( style ),
mOutlineThickness( outlineThickness ),
mCurFont( mFont ) {
mCurFont( mFont ),
mIsRTL( isRTL ) {
if ( mIsRTL )
mIndex = mString.size();
findNextEnd();
}
String::View TextShapeRun::curRun() const {
if ( mIsRTL )
return mString.substr( mIndex - mLen, mIsNewLine ? mLen - 1 : mLen );
return mString.substr( mIndex, mIsNewLine ? mLen - 1 : mLen );
}
bool TextShapeRun::hasNext() const {
if ( mIsRTL )
return mIndex > 0;
return mIndex < mString.size();
}
std::size_t TextShapeRun::pos() const {
if ( mIsRTL )
return mIndex - mLen;
return mIndex;
}
void TextShapeRun::next() {
mIndex += mLen;
if ( mIsRTL ) {
mIndex -= mLen;
} else {
mIndex += mLen;
}
findNextEnd();
}
@@ -46,60 +59,122 @@ FontTrueType* TextShapeRun::font() {
}
void TextShapeRun::findNextEnd() {
if ( !hasNext() ) {
mLen = 0;
return;
}
#ifdef EE_TEXT_SHAPER_ENABLED
Font* lFont = mStartFont ? mStartFont : mFont;
std::size_t len = mString.size();
std::size_t pos = 0;
if ( !mIsRTL ) {
Font* lFont = mStartFont ? mStartFont : mFont;
std::size_t len = mString.size();
std::size_t pos = 0;
for ( std::size_t idx = mIndex; idx < len; ++idx, ++pos ) {
auto ch = mString[idx];
auto font = mFont
->getGlyph( ch, mCharacterSize, mStyle & Text::Bold, mStyle & Text::Italic,
mOutlineThickness )
.font;
mIsNewLine = ( ch == '\n' );
for ( std::size_t idx = mIndex; idx < len; ++idx, ++pos ) {
auto ch = mString[idx];
auto font = mFont
->getGlyph( ch, mCharacterSize, mStyle & Text::Bold,
mStyle & Text::Italic, mOutlineThickness )
.font;
mIsNewLine = ( ch == '\n' );
if ( idx == mIndex ) {
mStartFont = font;
lFont = font;
mCurFont = font;
}
// Break run if:
// - Newline
// - Font changed
if ( mIsNewLine || ( lFont != nullptr && font != lFont ) ) {
mLen = mIsNewLine ? pos + 1 : pos;
mCurFont = lFont;
return;
}
if ( idx == mIndex ) {
mStartFont = font;
lFont = font;
mCurFont = font;
}
// Break run if:
// - Newline
// - Font changed
if ( mIsNewLine || ( lFont != nullptr && font != lFont ) ) {
mLen = mIsNewLine ? pos + 1 : pos;
mCurFont = lFont;
return;
mLen = len - mIndex;
} else {
Font* lFont = mStartFont ? mStartFont : mFont;
std::size_t pos = 0;
for ( ; pos < mIndex; ++pos ) {
std::size_t idx = mIndex - 1 - pos;
auto ch = mString[idx];
auto font = mFont
->getGlyph( ch, mCharacterSize, mStyle & Text::Bold,
mStyle & Text::Italic, mOutlineThickness )
.font;
mIsNewLine = ( ch == '\n' );
if ( pos == 0 ) {
mStartFont = font;
lFont = font;
mCurFont = font;
}
if ( mIsNewLine || ( lFont != nullptr && font != lFont ) ) {
mLen = mIsNewLine ? pos + 1 : pos;
mCurFont = lFont;
return;
}
lFont = font;
mCurFont = font;
}
lFont = font;
mCurFont = font;
mLen = mIndex;
}
mLen = len - mIndex;
#else
Font* lFont = mStartFont;
std::size_t len = mString.size();
std::size_t idx;
std::size_t pos = 0;
for ( idx = mIndex; idx < len; idx++, pos++ ) {
Font* font = mFont
->getGlyph( mString[idx], mCharacterSize, mStyle & Text::Bold,
mStyle & Text::Italic, mOutlineThickness )
.font;
mIsNewLine = mString[idx] == '\n';
if ( mIsNewLine || ( lFont != nullptr && font != lFont ) ) {
mCurFont = lFont;
mStartFont = font;
mLen = mIsNewLine ? pos + 1 : pos;
return;
if ( !mIsRTL ) {
Font* lFont = mStartFont;
std::size_t len = mString.size();
std::size_t pos = 0;
for ( std::size_t idx = mIndex; idx < len; idx++, pos++ ) {
Font* font = mFont
->getGlyph( mString[idx], mCharacterSize, mStyle & Text::Bold,
mStyle & Text::Italic, mOutlineThickness )
.font;
mIsNewLine = mString[idx] == '\n';
if ( pos == 0 )
lFont = font;
if ( mIsNewLine || ( lFont != nullptr && font != lFont ) ) {
mCurFont = lFont;
mStartFont = font;
mLen = mIsNewLine ? pos + 1 : pos;
return;
}
lFont = font;
mCurFont = font;
}
lFont = font;
mCurFont = font;
mLen = pos;
} else {
Font* lFont = mStartFont;
std::size_t pos = 0;
for ( ; pos < mIndex; ++pos ) {
std::size_t idx = mIndex - 1 - pos;
Font* font = mFont
->getGlyph( mString[idx], mCharacterSize, mStyle & Text::Bold,
mStyle & Text::Italic, mOutlineThickness )
.font;
mIsNewLine = mString[idx] == '\n';
if ( pos == 0 )
lFont = font;
if ( mIsNewLine || ( lFont != nullptr && font != lFont ) ) {
mCurFont = lFont;
mStartFont = font;
mLen = mIsNewLine ? pos + 1 : pos;
return;
}
lFont = font;
mCurFont = font;
}
mLen = mIndex;
}
mLen = idx;
#endif
}
+5 -4
View File
@@ -166,17 +166,18 @@ EE_MAIN_FUNC int main( int, char*[] ) {
editor->setLayoutSizePolicy( SizePolicy::MatchParent, SizePolicy::MatchParent );
editor->setParent( ll );
editor->setFontSize( PixelDensity::dpToPx( 12 ) );
// FontManager::instance()->addFallbackFont(
// FontTrueType::New( "arabic", "unit_tests/assets/fonts/NotoNaskhArabic-Regular.ttf" ) );
FontManager::instance()->addFallbackFont(
FontTrueType::New( "arabic", "unit_tests/assets/fonts/NotoNaskhArabic-Regular.ttf" ) );
FontManager::instance()->addFallbackFont( FontTrueType::New(
"NotoSerifBengali-Regular", "unit_tests/assets/fonts/NotoSansBengali-Regular.ttf" ) );
// editor->setLineWrapMode( LineWrapMode::Word );
// editor->setFont( FontManager::instance()->getByName( "monospace" ) );
editor->setFont( FontManager::instance()->getByName( "monospace" ) );
// editor->loadFromFile( "unit_tests/assets/textfiles/test-arabic-simple.uext" );
// editor->loadFromFile( "unit_tests/assets/textfiles/test-arabic.uext" );
editor->loadFromFile( "unit_tests/assets/textfiles/test-bengali.uext" );
// editor->loadFromFile( "unit_tests/assets/textfiles/test-bengali.uext" );
// editor->loadFromFile( "unit_tests/assets/textfiles/test-flags.uext" );
// editor->loadFromFile( "unit_tests/assets/textformat/english.utf8.lf.nobom.txt" );
editor->loadFromFile( "unit_tests/assets/textfiles/test-arabic-mixed.uext" );
// editor->getDocument().textInput( "اسمي..." );
// editor->getDocument().textInput( " হ্যাঁ " );
// editor->getDocument().textInput( "I'm from...: আমি ... থেকে এসেছি।" );