mirror of
https://github.com/SpartanJ/eepp.git
synced 2026-10-27 00:28:34 +03:00
Fix RTL text rendering.
This commit is contained in:
Binary file not shown.
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: 😎].
|
||||
@@ -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
|
||||
|
||||
@@ -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(), ¶graphLength,
|
||||
&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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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...: আমি ... থেকে এসেছি।" );
|
||||
|
||||
Reference in New Issue
Block a user