2025-10-16 21:22:08 +03:00
# include "RichTextBox.hpp"
2025-11-04 10:58:26 +03:00
# include "TextStyleModifier.hpp"
2025-10-18 13:32:12 +03:00
# include <limits>
2025-10-20 11:41:22 +03:00
# include <modules/skparagraph/include/DartTypes.h>
2025-10-18 13:32:12 +03:00
# include <modules/skparagraph/include/Metrics.h>
# include <modules/skparagraph/src/TextLine.h>
# include <modules/skunicode/include/SkUnicode.h>
2025-10-16 21:22:08 +03:00
# include <modules/skunicode/include/SkUnicode_icu.h>
# include <modules/skparagraph/src/ParagraphBuilderImpl.h>
# include <modules/skparagraph/src/ParagraphImpl.h>
# include <Helpers/MathExtras.hpp>
2025-10-18 13:32:12 +03:00
# include <src/base/SkUTF.h>
2025-11-04 10:58:26 +03:00
# include <cereal/types/string.hpp>
2025-11-04 17:31:23 +03:00
void RichTextBox : : TextStyleRangeModifier : : save ( cereal : : PortableBinaryOutputArchive & a ) const {
a ( start , end , modifier - > get_type ( ) , * modifier ) ;
2025-11-04 10:58:26 +03:00
}
2025-11-04 17:31:23 +03:00
void RichTextBox : : TextStyleRangeModifier : : load ( cereal : : PortableBinaryInputArchive & a ) {
a ( start , end ) ;
TextStyleModifier : : ModifierType mType ;
a ( mType ) ;
modifier = TextStyleModifier : : allocate_modifier ( mType ) ;
a ( * modifier ) ;
}
void RichTextBox : : TextStyleRangeModifier : : apply_to_start_and_end ( const std : : function < void ( TextPosition & ) > & f ) {
f ( start ) ;
f ( end ) ;
2025-11-04 10:58:26 +03:00
}
2025-10-18 13:32:12 +03:00
bool RichTextBox : : TextPosition : : operator < ( const RichTextBox : : TextPosition & o ) const {
return ( this - > fParagraphIndex < o . fParagraphIndex ) | | ( this - > fParagraphIndex = = o . fParagraphIndex & & this - > fTextByteIndex < o . fTextByteIndex ) ;
}
2025-10-16 21:22:08 +03:00
2025-11-04 17:31:23 +03:00
bool RichTextBox : : TextPosition : : operator > ( const RichTextBox : : TextPosition & o ) const {
return ! ( * this < o ) & & ! ( * this = = o ) ;
}
2025-11-04 19:04:09 +03:00
bool RichTextBox : : TextPosition : : operator > = ( const RichTextBox : : TextPosition & o ) const {
return ( * this > o ) | | ( * this = = o ) ;
}
bool RichTextBox : : TextPosition : : operator < = ( const RichTextBox : : TextPosition & o ) const {
return ( * this < o ) | | ( * this = = o ) ;
}
2025-10-16 21:22:08 +03:00
RichTextBox : : RichTextBox ( ) {
2025-10-19 15:54:00 +03:00
paragraphs . emplace_back ( ) ;
2025-10-16 21:22:08 +03:00
}
void RichTextBox : : process_key_input ( Cursor & cur , InputKey in , bool ctrl , bool shift ) {
bool moved = false ;
switch ( in ) {
case InputKey : : LEFT :
2025-10-20 11:41:22 +03:00
cur . pos = cur . selectionBeginPos = move ( ctrl ? Movement : : LEFT_WORD : Movement : : LEFT , cur . pos , nullptr , true ) ;
2025-10-16 21:22:08 +03:00
moved = true ;
break ;
case InputKey : : RIGHT :
2025-10-20 11:41:22 +03:00
cur . pos = cur . selectionBeginPos = move ( ctrl ? Movement : : RIGHT_WORD : Movement : : RIGHT , cur . pos , nullptr , true ) ;
2025-10-16 21:22:08 +03:00
moved = true ;
break ;
case InputKey : : UP :
2025-10-18 13:32:12 +03:00
cur . pos = cur . selectionBeginPos = move ( Movement : : UP , cur . pos , & cur . previousX ) ;
2025-10-16 21:22:08 +03:00
moved = true ;
break ;
case InputKey : : DOWN :
2025-10-18 13:32:12 +03:00
cur . pos = cur . selectionBeginPos = move ( Movement : : DOWN , cur . pos , & cur . previousX ) ;
2025-10-16 21:22:08 +03:00
moved = true ;
break ;
case InputKey : : HOME :
cur . pos = cur . selectionBeginPos = move ( Movement : : HOME , cur . pos ) ;
moved = true ;
break ;
case InputKey : : END :
cur . pos = cur . selectionBeginPos = move ( Movement : : END , cur . pos ) ;
moved = true ;
break ;
case InputKey : : BACKSPACE :
if ( cur . selectionBeginPos ! = cur . selectionEndPos )
cur . selectionEndPos = cur . selectionBeginPos = cur . pos = remove ( cur . selectionBeginPos , cur . selectionEndPos ) ;
else
2025-11-04 10:58:26 +03:00
cur . selectionEndPos = cur . selectionBeginPos = cur . pos = remove ( cur . pos , move ( ctrl ? Movement : : LEFT_WORD : Movement : : LEFT , cur . pos ) ) ;
2025-10-16 21:22:08 +03:00
break ;
case InputKey : : DELETE :
if ( cur . selectionBeginPos ! = cur . selectionEndPos )
cur . selectionEndPos = cur . selectionBeginPos = cur . pos = remove ( cur . selectionBeginPos , cur . selectionEndPos ) ;
else
2025-11-04 10:58:26 +03:00
cur . selectionEndPos = cur . selectionBeginPos = cur . pos = remove ( cur . pos , move ( ctrl ? Movement : : RIGHT_WORD : Movement : : RIGHT , cur . pos ) ) ;
2025-10-16 21:22:08 +03:00
break ;
case InputKey : : ENTER :
2025-10-20 17:30:36 +03:00
process_text_input ( cur , " \n " ) ;
break ;
case InputKey : : TAB :
process_text_input ( cur , " \t " ) ;
2025-10-16 21:22:08 +03:00
break ;
case InputKey : : SELECT_ALL :
2025-11-04 17:31:23 +03:00
cur . selectionBeginPos = move ( Movement : : HOME , cur . pos ) ;
cur . pos = cur . selectionEndPos = move ( Movement : : END , cur . pos ) ;
2025-10-16 21:22:08 +03:00
break ;
}
if ( moved & & ! shift )
cur . selectionEndPos = cur . selectionBeginPos ;
2025-10-18 13:32:12 +03:00
if ( in ! = InputKey : : UP & & in ! = InputKey : : DOWN )
cur . previousX = std : : nullopt ;
}
void RichTextBox : : process_mouse_left_button ( Cursor & cur , const Vector2f & pos , bool clicked , bool held , bool shift ) {
if ( clicked | | held ) {
rebuild ( ) ;
2025-10-19 15:54:00 +03:00
cur . pos = get_text_pos_closest_to_point ( pos ) ;
cur . selectionBeginPos = cur . pos ;
if ( clicked & & ! shift )
cur . selectionEndPos = cur . selectionBeginPos ;
cur . previousX = std : : nullopt ;
2025-10-18 13:32:12 +03:00
}
2025-10-16 21:22:08 +03:00
}
std : : string RichTextBox : : process_copy ( Cursor & cur ) {
2025-10-19 15:54:00 +03:00
return get_text_between ( cur . selectionBeginPos , cur . selectionEndPos ) ;
2025-10-16 21:22:08 +03:00
}
std : : string RichTextBox : : process_cut ( Cursor & cur ) {
std : : string toRet = process_copy ( cur ) ;
2025-10-18 13:32:12 +03:00
if ( cur . selectionBeginPos ! = cur . selectionEndPos ) {
2025-10-16 21:22:08 +03:00
cur . selectionEndPos = cur . selectionBeginPos = cur . pos = remove ( cur . selectionBeginPos , cur . selectionEndPos ) ;
2025-10-18 13:32:12 +03:00
cur . previousX = std : : nullopt ;
}
2025-10-16 21:22:08 +03:00
return toRet ;
}
void RichTextBox : : process_text_input ( Cursor & cur , const std : : string & in ) {
if ( ! in . empty ( ) ) {
if ( cur . selectionBeginPos ! = cur . selectionEndPos )
cur . selectionEndPos = cur . selectionBeginPos = cur . pos = remove ( cur . selectionBeginPos , cur . selectionEndPos ) ;
cur . selectionEndPos = cur . selectionBeginPos = cur . pos = insert ( cur . pos , in ) ;
2025-10-18 13:32:12 +03:00
cur . previousX = std : : nullopt ;
}
}
2025-10-19 15:54:00 +03:00
size_t RichTextBox : : count_grapheme ( const std : : string & text ) {
2025-10-18 13:32:12 +03:00
auto u = SkUnicodes : : ICU : : Make ( ) ;
auto breakIterator = u - > makeBreakIterator ( SkUnicode : : BreakType : : kGraphemes ) ;
breakIterator - > setText ( text . c_str ( ) , text . size ( ) ) ;
size_t count = 0 ;
while ( ! breakIterator - > isDone ( ) ) {
breakIterator - > next ( ) ;
count + + ;
}
return count - 1 ;
}
2025-10-19 15:54:00 +03:00
size_t RichTextBox : : next_grapheme ( const std : : string & text , size_t textBytePos ) {
2025-10-18 13:32:12 +03:00
if ( textBytePos = = text . size ( ) )
return text . size ( ) ;
auto u = SkUnicodes : : ICU : : Make ( ) ;
auto breakIterator = u - > makeBreakIterator ( SkUnicode : : BreakType : : kGraphemes ) ;
breakIterator - > setText ( text . c_str ( ) + textBytePos , text . size ( ) - textBytePos ) ;
size_t count = breakIterator - > next ( ) ;
return count + textBytePos ;
}
2025-10-19 15:54:00 +03:00
size_t RichTextBox : : prev_grapheme ( const std : : string & text , size_t textBytePos ) {
2025-10-18 13:32:12 +03:00
if ( textBytePos = = 0 )
return 0 ;
2025-10-19 15:54:00 +03:00
auto u = SkUnicodes : : ICU : : Make ( ) ;
auto breakIterator = u - > makeBreakIterator ( SkUnicode : : BreakType : : kGraphemes ) ;
breakIterator - > setText ( text . c_str ( ) , text . size ( ) ) ;
size_t toRet = 0 ;
for ( ; ; ) {
size_t potentialNext = breakIterator - > next ( ) ;
if ( potentialNext > = textBytePos )
return toRet ;
toRet = potentialNext ;
}
}
RichTextBox : : TextPosition RichTextBox : : get_text_pos_from_byte_pos ( const std : : string & text , size_t textBytePos ) {
TextPosition toRet { 0 , 0 } ;
for ( size_t i = 0 ; i < textBytePos ; i + + ) {
char c = text [ i ] ;
if ( c = = ' \n ' ) {
toRet . fTextByteIndex = 0 ;
toRet . fParagraphIndex + + ;
}
else
toRet . fTextByteIndex + + ;
}
return toRet ;
2025-10-18 13:32:12 +03:00
}
2025-10-19 15:54:00 +03:00
size_t RichTextBox : : get_byte_pos_from_text_pos ( TextPosition textPos ) {
size_t toRet = 0 ;
for ( size_t pIndex = 0 ; pIndex < textPos . fParagraphIndex ; pIndex + + )
toRet + = paragraphs [ pIndex ] . text . size ( ) + 1 ; // Add 1 byte for newline
toRet + = textPos . fTextByteIndex ;
return toRet ;
}
RichTextBox : : TextPosition RichTextBox : : get_text_pos_closest_to_point ( Vector2f point ) {
rebuild ( ) ;
2025-10-18 13:32:12 +03:00
skia : : textlayout : : Paragraph : : GlyphClusterInfo glyphInfo ;
2025-10-19 15:54:00 +03:00
size_t pIndex = 0 ;
for ( ; pIndex < ( paragraphs . size ( ) - 1 ) & & point . y ( ) > paragraphs [ pIndex ] . p - > getHeight ( ) ; pIndex + + )
point . y ( ) - = paragraphs [ pIndex ] . p - > getHeight ( ) ;
ParagraphData & pData = paragraphs [ pIndex ] ;
bool foundGlyph = pData . p - > getClosestGlyphClusterAt ( point . x ( ) , point . y ( ) , & glyphInfo ) ;
2025-10-18 13:32:12 +03:00
if ( foundGlyph ) {
2025-11-04 17:31:23 +03:00
TextPosition toRet = { pIndex , 0 } ;
2025-10-18 13:32:12 +03:00
toRet . fTextByteIndex = glyphInfo . fClusterTextRange . start ;
2025-10-20 11:41:22 +03:00
if ( glyphInfo . fGlyphClusterPosition = = skia : : textlayout : : TextDirection : : kLtr ) {
if ( std : : abs ( glyphInfo . fBounds . right ( ) - point . x ( ) ) < std : : abs ( glyphInfo . fBounds . left ( ) - point . x ( ) ) )
toRet . fTextByteIndex = next_grapheme ( pData . text , toRet . fTextByteIndex ) ;
}
else {
if ( std : : abs ( glyphInfo . fBounds . left ( ) - point . x ( ) ) < std : : abs ( glyphInfo . fBounds . right ( ) - point . x ( ) ) )
toRet . fTextByteIndex = next_grapheme ( pData . text , toRet . fTextByteIndex ) ;
}
// Space at the end of a line will bring the cursor to the next line, check if the cursor moved to the next line
if ( toRet . fTextByteIndex ! = pData . text . size ( ) ) {
skia : : textlayout : : LineMetrics lineMetrics ;
int lineNumber = pData . p - > getLineNumberAt ( toRet . fTextByteIndex ) ;
if ( lineNumber > 0 ) {
bool lineMetricsFound = pData . p - > getLineMetricsAt ( lineNumber , & lineMetrics ) ;
if ( lineMetricsFound ) {
float top = lineMetrics . fBaseline - lineMetrics . fAscent ;
if ( point . y ( ) < top )
toRet . fTextByteIndex = prev_grapheme ( pData . text , toRet . fTextByteIndex ) ;
}
}
}
return toRet ;
2025-10-16 21:22:08 +03:00
}
2025-10-19 15:54:00 +03:00
else
2025-11-04 17:31:23 +03:00
return TextPosition { pIndex , 0 } ;
2025-10-19 15:54:00 +03:00
}
2025-10-25 17:31:55 +03:00
RichTextBox : : RichTextData RichTextBox : : get_rich_text_data ( ) {
2025-11-04 10:58:26 +03:00
RichTextData toRet ;
for ( auto & p : paragraphs ) {
toRet . paragraphs . emplace_back ( ) ;
toRet . paragraphs . back ( ) . text = p . text ;
}
2025-11-04 17:31:23 +03:00
toRet . tStyleModifiers = tStyleModifiers ;
2025-11-04 10:58:26 +03:00
return toRet ;
2025-10-25 17:31:55 +03:00
}
void RichTextBox : : clear_text ( ) {
paragraphs . clear ( ) ;
2025-11-04 17:31:23 +03:00
tStyleModifiers . clear ( ) ;
2025-10-25 17:31:55 +03:00
paragraphs . emplace_back ( ) ;
}
void RichTextBox : : set_string ( const std : : string & str ) {
clear_text ( ) ;
insert ( { 0 , 0 } , str ) ;
}
void RichTextBox : : set_rich_text_data ( const RichTextData & richText ) {
2025-11-04 10:58:26 +03:00
paragraphs . clear ( ) ;
for ( auto & p : richText . paragraphs ) {
paragraphs . emplace_back ( ) ;
paragraphs . back ( ) . text = p . text ;
}
if ( richText . paragraphs . empty ( ) )
paragraphs . emplace_back ( ) ;
2025-11-04 17:31:23 +03:00
tStyleModifiers = richText . tStyleModifiers ;
fit_text_style_ranges_in_text ( ) ;
2025-11-04 10:58:26 +03:00
needsRebuild = true ;
2025-10-25 17:31:55 +03:00
}
2025-10-20 17:30:36 +03:00
std : : string RichTextBox : : get_string ( ) {
return get_text_between ( { 0 , 0 } , move ( RichTextBox : : Movement : : END , { 0 , 0 } ) ) ;
}
2025-10-19 15:54:00 +03:00
std : : string RichTextBox : : get_text_between ( TextPosition p1 , TextPosition p2 ) {
p1 = move ( Movement : : NOWHERE , p1 ) ;
p2 = move ( Movement : : NOWHERE , p2 ) ;
if ( p1 = = p2 )
return " " ;
TextPosition start = std : : min ( p1 , p2 ) ;
TextPosition end = std : : max ( p1 , p2 ) ;
std : : string toRet ;
for ( size_t pIndex = start . fParagraphIndex ; pIndex < = end . fParagraphIndex ; pIndex + + ) {
size_t textIndexStart = pIndex = = start . fParagraphIndex ? start . fTextByteIndex : 0 ;
size_t textIndexEnd = pIndex = = end . fParagraphIndex ? end . fTextByteIndex : paragraphs [ pIndex ] . text . size ( ) ;
toRet + = paragraphs [ pIndex ] . text . substr ( textIndexStart , textIndexEnd - textIndexStart ) ;
if ( pIndex ! = end . fParagraphIndex )
toRet + = ' \n ' ;
}
return toRet ;
}
2025-11-04 17:31:23 +03:00
void RichTextBox : : set_initial_text_style ( const skia : : textlayout : : TextStyle & tStyle ) {
if ( ! tStyle . equals ( initialTStyle ) ) {
initialTStyle = tStyle ;
needsRebuild = true ;
}
2025-11-04 10:58:26 +03:00
}
void RichTextBox : : set_text_style_modifier_between ( TextPosition p1 , TextPosition p2 , const std : : shared_ptr < TextStyleModifier > & modifier ) {
2025-11-04 17:31:23 +03:00
p1 = move ( Movement : : NOWHERE , p1 ) ;
p2 = move ( Movement : : NOWHERE , p2 ) ;
if ( p1 ! = p2 ) {
fit_text_style_ranges_in_text ( ) ;
TextStyleRangeModifier toPlace ;
toPlace . start = std : : min ( p1 , p2 ) ;
toPlace . end = std : : max ( p1 , p2 ) ;
toPlace . modifier = modifier ;
2025-11-04 19:04:09 +03:00
std : : erase_if ( tStyleModifiers , [ & toPlace ] ( TextStyleRangeModifier & tStyleMod ) {
if ( tStyleMod . modifier - > equivalent ( * toPlace . modifier ) ) {
// The equivalent modifiers collide, so merge them into one modifier
if ( ( tStyleMod . start > = toPlace . start & & tStyleMod . start < = toPlace . end ) | | ( tStyleMod . end > = toPlace . start & & tStyleMod . end < = toPlace . end ) | |
( toPlace . start > = tStyleMod . start & & toPlace . start < = tStyleMod . end ) | | ( toPlace . end > = tStyleMod . start & & toPlace . end < = tStyleMod . end ) ) {
toPlace . start = std : : min ( toPlace . start , tStyleMod . start ) ;
toPlace . end = std : : max ( toPlace . end , tStyleMod . end ) ;
return true ;
}
}
return false ;
} ) ;
std : : erase_if ( tStyleModifiers , [ & toPlace ] ( TextStyleRangeModifier & tStyleMod ) {
if ( tStyleMod . modifier - > get_type ( ) = = toPlace . modifier - > get_type ( ) ) {
bool tStyleModStartInToPlace = ( tStyleMod . start > = toPlace . start & & tStyleMod . start < = toPlace . end ) ;
bool tStyleModEndInToPlace = ( tStyleMod . end > = toPlace . start & & tStyleMod . end < = toPlace . end ) ;
if ( tStyleModStartInToPlace & & tStyleModEndInToPlace )
return true ;
else if ( tStyleModStartInToPlace )
tStyleMod . start = toPlace . end ;
else if ( tStyleModEndInToPlace )
tStyleMod . end = toPlace . start ;
}
return false ;
} ) ;
2025-11-04 17:31:23 +03:00
tStyleModifiers . emplace_back ( toPlace ) ;
2025-11-04 19:04:09 +03:00
fit_text_style_ranges_in_text ( ) ;
2025-11-04 17:31:23 +03:00
needsRebuild = true ;
}
2025-11-04 10:58:26 +03:00
}
2025-10-20 11:41:22 +03:00
RichTextBox : : TextPosition RichTextBox : : move ( Movement movement , TextPosition pos , std : : optional < float > * previousX , bool flipDependingOnTextDirection ) {
2025-10-19 15:54:00 +03:00
if ( pos . fParagraphIndex > = paragraphs . size ( ) ) {
pos . fParagraphIndex = paragraphs . size ( ) - 1 ;
pos . fTextByteIndex = paragraphs [ pos . fParagraphIndex ] . text . size ( ) ;
}
if ( pos . fTextByteIndex > = paragraphs [ pos . fParagraphIndex ] . text . size ( ) )
pos . fTextByteIndex = paragraphs [ pos . fParagraphIndex ] . text . size ( ) ;
2025-10-16 21:22:08 +03:00
2025-10-25 17:31:55 +03:00
if ( movement ! = Movement : : NOWHERE & & movement ! = Movement : : HOME & & movement ! = Movement : : END ) {
rebuild ( ) ;
if ( flipDependingOnTextDirection & & paragraphs [ pos . fParagraphIndex ] . pStyle . getTextDirection ( ) = = skia : : textlayout : : TextDirection : : kRtl ) {
if ( movement = = Movement : : LEFT )
movement = Movement : : RIGHT ;
else if ( movement = = Movement : : RIGHT )
movement = Movement : : LEFT ;
else if ( movement = = Movement : : LEFT_WORD )
movement = Movement : : RIGHT_WORD ;
else if ( movement = = Movement : : RIGHT_WORD )
movement = Movement : : LEFT_WORD ;
}
2025-10-20 11:41:22 +03:00
}
2025-10-16 21:22:08 +03:00
switch ( movement ) {
2025-10-19 15:54:00 +03:00
case Movement : : NOWHERE :
break ;
2025-10-18 13:32:12 +03:00
case Movement : : LEFT : {
2025-10-19 15:54:00 +03:00
if ( pos . fTextByteIndex = = 0 ) {
if ( pos . fParagraphIndex ! = 0 ) {
pos . fParagraphIndex - - ;
pos . fTextByteIndex = paragraphs [ pos . fParagraphIndex ] . text . size ( ) ;
}
}
else
pos . fTextByteIndex = prev_grapheme ( paragraphs [ pos . fParagraphIndex ] . text , pos . fTextByteIndex ) ;
2025-10-16 21:22:08 +03:00
break ;
2025-10-18 13:32:12 +03:00
}
case Movement : : RIGHT : {
2025-10-19 15:54:00 +03:00
if ( pos . fTextByteIndex = = paragraphs [ pos . fParagraphIndex ] . text . size ( ) ) {
if ( pos . fParagraphIndex ! = paragraphs . size ( ) - 1 ) {
pos . fParagraphIndex + + ;
pos . fTextByteIndex = 0 ;
}
}
else
pos . fTextByteIndex = next_grapheme ( paragraphs [ pos . fParagraphIndex ] . text , pos . fTextByteIndex ) ;
2025-10-16 21:22:08 +03:00
break ;
2025-10-18 13:32:12 +03:00
}
2025-10-16 21:22:08 +03:00
case Movement : : UP : {
2025-10-18 13:32:12 +03:00
assert ( previousX ! = nullptr ) ;
2025-10-19 15:54:00 +03:00
int lineNumber = paragraphs [ pos . fParagraphIndex ] . p - > getLineNumberAt ( pos . fTextByteIndex = = paragraphs [ pos . fParagraphIndex ] . text . size ( ) ? prev_grapheme ( paragraphs [ pos . fParagraphIndex ] . text , pos . fTextByteIndex ) : pos . fTextByteIndex ) ;
if ( lineNumber < = 0 & & pos . fParagraphIndex = = 0 ) {
2025-10-18 13:32:12 +03:00
pos . fTextByteIndex = 0 ;
if ( previousX ! = nullptr )
* previousX = get_cursor_rect ( pos ) . centerX ( ) ;
}
else {
2025-10-19 15:54:00 +03:00
if ( lineNumber < = 0 & & paragraphs [ pos . fParagraphIndex - 1 ] . text . empty ( ) ) {
if ( previousX ! = nullptr & & ! previousX - > has_value ( ) )
* previousX = get_cursor_rect ( pos ) . centerX ( ) ;
2025-11-04 17:31:23 +03:00
pos = TextPosition { pos . fParagraphIndex - 1 , 0 } ;
2025-10-19 15:54:00 +03:00
}
else {
skia : : textlayout : : LineMetrics prevLineMetrics ;
SkRect cursorRect = get_cursor_rect ( pos ) ;
if ( lineNumber < = 0 ) {
pos . fParagraphIndex - - ;
lineNumber = paragraphs [ pos . fParagraphIndex ] . p - > lineNumber ( ) ;
}
paragraphs [ pos . fParagraphIndex ] . p - > getLineMetricsAt ( lineNumber - 1 , & prevLineMetrics ) ;
Vector2f pointToCheck = { ( previousX ! = nullptr & & previousX - > has_value ( ) ) ? previousX - > value ( ) : cursorRect . centerX ( ) , prevLineMetrics . fBaseline + paragraphs [ pos . fParagraphIndex ] . heightOffset } ;
pos = get_text_pos_closest_to_point ( pointToCheck ) ;
if ( previousX ! = nullptr & & ! previousX - > has_value ( ) )
* previousX = pointToCheck . x ( ) ;
2025-10-18 13:32:12 +03:00
}
}
2025-10-16 21:22:08 +03:00
break ;
}
case Movement : : DOWN : {
2025-10-18 13:32:12 +03:00
assert ( previousX ! = nullptr ) ;
2025-10-19 15:54:00 +03:00
int lineNumber = paragraphs [ pos . fParagraphIndex ] . p - > getLineNumberAt ( pos . fTextByteIndex ) ;
if ( ( lineNumber = = - 1 | | lineNumber = = static_cast < int > ( paragraphs [ pos . fParagraphIndex ] . p - > lineNumber ( ) - 1 ) ) & & pos . fParagraphIndex = = ( paragraphs . size ( ) - 1 ) ) {
pos . fTextByteIndex = paragraphs [ pos . fParagraphIndex ] . text . size ( ) ;
2025-10-18 13:32:12 +03:00
if ( previousX ! = nullptr )
* previousX = get_cursor_rect ( pos ) . centerX ( ) ;
}
else {
2025-10-19 15:54:00 +03:00
if ( ( lineNumber = = - 1 | | lineNumber = = static_cast < int > ( paragraphs [ pos . fParagraphIndex ] . p - > lineNumber ( ) - 1 ) ) & & paragraphs [ pos . fParagraphIndex + 1 ] . text . empty ( ) ) {
if ( previousX ! = nullptr & & ! previousX - > has_value ( ) )
* previousX = get_cursor_rect ( pos ) . centerX ( ) ;
2025-11-04 17:31:23 +03:00
pos = TextPosition { pos . fParagraphIndex + 1 , 0 } ;
2025-10-19 15:54:00 +03:00
}
else {
skia : : textlayout : : LineMetrics nextLineMetrics ;
SkRect cursorRect = get_cursor_rect ( pos ) ;
if ( lineNumber = = - 1 | | lineNumber = = static_cast < int > ( paragraphs [ pos . fParagraphIndex ] . p - > lineNumber ( ) - 1 ) ) {
pos . fParagraphIndex + + ;
lineNumber = - 1 ;
}
paragraphs [ pos . fParagraphIndex ] . p - > getLineMetricsAt ( lineNumber + 1 , & nextLineMetrics ) ;
Vector2f pointToCheck = { ( previousX ! = nullptr & & previousX - > has_value ( ) ) ? previousX - > value ( ) : cursorRect . centerX ( ) , nextLineMetrics . fBaseline + paragraphs [ pos . fParagraphIndex ] . heightOffset } ;
pos = get_text_pos_closest_to_point ( pointToCheck ) ;
if ( previousX ! = nullptr & & ! previousX - > has_value ( ) )
* previousX = pointToCheck . x ( ) ;
2025-10-18 13:32:12 +03:00
}
}
break ;
}
case Movement : : LEFT_WORD : {
2025-10-19 15:54:00 +03:00
if ( pos = = TextPosition { 0 , 0 } )
2025-10-18 13:32:12 +03:00
break ;
2025-10-19 15:54:00 +03:00
2025-10-20 17:30:36 +03:00
std : : string fullText = get_string ( ) ;
2025-10-19 15:54:00 +03:00
size_t bytePosInFullText = get_byte_pos_from_text_pos ( pos ) ;
2025-10-18 13:32:12 +03:00
auto u = SkUnicodes : : ICU : : Make ( ) ;
size_t byteIndexToRet = 0 ;
auto breakIterator = u - > makeBreakIterator ( SkUnicode : : BreakType : : kWords ) ;
2025-10-19 15:54:00 +03:00
breakIterator - > setText ( fullText . c_str ( ) , fullText . size ( ) ) ;
2025-10-18 13:32:12 +03:00
while ( ! breakIterator - > isDone ( ) ) {
size_t nextByteIndex = breakIterator - > next ( ) ;
2025-10-19 15:54:00 +03:00
if ( nextByteIndex > = bytePosInFullText )
return get_text_pos_from_byte_pos ( fullText , byteIndexToRet ) ;
2025-10-18 13:32:12 +03:00
2025-10-19 15:54:00 +03:00
const char * tPtr = fullText . c_str ( ) + nextByteIndex ;
SkUnichar u = SkUTF : : NextUTF8 ( & tPtr , fullText . c_str ( ) + fullText . size ( ) ) ;
2025-10-18 13:32:12 +03:00
if ( ! SkUnicodes : : ICU : : Make ( ) - > isWhitespace ( u ) )
byteIndexToRet = nextByteIndex ;
}
break ;
}
case Movement : : RIGHT_WORD : {
2025-10-19 15:54:00 +03:00
TextPosition endPos = move ( Movement : : END , { 0 , 0 } ) ;
if ( pos = = endPos )
break ;
2025-10-20 17:30:36 +03:00
std : : string fullText = get_string ( ) ;
2025-10-19 15:54:00 +03:00
size_t bytePosInFullText = get_byte_pos_from_text_pos ( pos ) ;
2025-10-18 13:32:12 +03:00
auto u = SkUnicodes : : ICU : : Make ( ) ;
auto breakIterator = u - > makeBreakIterator ( SkUnicode : : BreakType : : kWords ) ;
2025-10-19 15:54:00 +03:00
breakIterator - > setText ( fullText . c_str ( ) + bytePosInFullText , fullText . size ( ) - bytePosInFullText ) ;
2025-10-18 13:32:12 +03:00
while ( ! breakIterator - > isDone ( ) ) {
2025-10-19 15:54:00 +03:00
size_t p = breakIterator - > next ( ) + bytePosInFullText ;
if ( p = = fullText . size ( ) )
return get_text_pos_from_byte_pos ( fullText , p ) ;
const char * tPtr = fullText . c_str ( ) + p ;
SkUnichar u = SkUTF : : NextUTF8 ( & tPtr , fullText . c_str ( ) + fullText . size ( ) ) ;
2025-10-18 13:32:12 +03:00
if ( ! SkUnicodes : : ICU : : Make ( ) - > isWhitespace ( u ) )
2025-10-19 15:54:00 +03:00
return get_text_pos_from_byte_pos ( fullText , p ) ;
2025-10-18 13:32:12 +03:00
}
2025-10-19 15:54:00 +03:00
2025-10-16 21:22:08 +03:00
break ;
}
case Movement : : HOME :
2025-10-18 13:32:12 +03:00
pos . fTextByteIndex = 0 ;
2025-10-19 15:54:00 +03:00
pos . fParagraphIndex = 0 ;
2025-10-16 21:22:08 +03:00
break ;
case Movement : : END :
2025-10-19 15:54:00 +03:00
pos . fTextByteIndex = paragraphs . back ( ) . text . size ( ) ;
pos . fParagraphIndex = paragraphs . size ( ) - 1 ;
2025-10-16 21:22:08 +03:00
break ;
}
return pos ;
}
void RichTextBox : : set_width ( float newWidth ) {
2025-10-18 13:32:12 +03:00
if ( width ! = newWidth ) {
width = newWidth ;
needsRebuild = true ;
}
2025-10-16 21:22:08 +03:00
}
void RichTextBox : : set_allow_newlines ( bool allow ) {
2025-10-20 17:30:36 +03:00
newlinesAllowed = allow ;
2025-10-16 21:22:08 +03:00
}
void RichTextBox : : set_font_collection ( const sk_sp < skia : : textlayout : : FontCollection > & fC ) {
2025-10-25 17:31:55 +03:00
if ( fontCollection ! = fC ) {
fontCollection = fC ;
needsRebuild = true ;
}
2025-10-16 21:22:08 +03:00
}
2025-11-04 17:31:23 +03:00
void RichTextBox : : fit_text_style_ranges_in_text ( ) {
std : : erase_if ( tStyleModifiers , [ & ] ( TextStyleRangeModifier & tStyleMod ) {
tStyleMod . start = move ( Movement : : NOWHERE , tStyleMod . start ) ;
tStyleMod . end = move ( Movement : : NOWHERE , tStyleMod . end ) ;
return tStyleMod . start = = tStyleMod . end ;
} ) ;
}
2025-10-16 21:22:08 +03:00
void RichTextBox : : rebuild ( ) {
if ( needsRebuild ) {
2025-10-19 15:54:00 +03:00
float heightOffset = 0.0f ;
2025-11-04 17:31:23 +03:00
std : : map < TextPosition , std : : vector < std : : shared_ptr < TextStyleModifier > > > modifierStackCommands ;
for ( TextStyleRangeModifier & modifierRange : tStyleModifiers ) {
modifierStackCommands [ modifierRange . start ] . emplace_back ( modifierRange . modifier ) ;
modifierStackCommands [ modifierRange . end ] . emplace_back ( modifierRange . modifier ) ;
}
auto nextStackCommandIt = modifierStackCommands . begin ( ) ;
2025-11-04 19:04:09 +03:00
std : : unordered_map < TextStyleModifier : : ModifierType , std : : vector < std : : shared_ptr < TextStyleModifier > > > modifierStacks ; // Not really a stack, but we use the last element as the modifier
2025-11-04 10:58:26 +03:00
2025-11-04 17:31:23 +03:00
skia : : textlayout : : TextStyle tStyle = initialTStyle ;
for ( size_t pIndex = 0 ; pIndex < paragraphs . size ( ) ; pIndex + + ) {
ParagraphData & pData = paragraphs [ pIndex ] ;
2025-10-20 11:41:22 +03:00
pData . pStyle . setTextAlign ( skia : : textlayout : : TextAlign : : kLeft ) ;
pData . pStyle . setTextDirection ( skia : : textlayout : : TextDirection : : kLtr ) ;
2025-11-04 17:31:23 +03:00
pData . pStyle . setTextStyle ( tStyle ) ;
size_t tIndex = 0 ;
2025-10-20 11:41:22 +03:00
skia : : textlayout : : ParagraphBuilderImpl a ( pData . pStyle , fontCollection , SkUnicodes : : ICU : : Make ( ) ) ;
2025-11-04 10:58:26 +03:00
2025-11-04 17:31:23 +03:00
for ( ; ; ) {
if ( nextStackCommandIt = = modifierStackCommands . end ( ) | | nextStackCommandIt - > first . fParagraphIndex ! = pIndex ) {
a . pushStyle ( tStyle ) ;
a . addText ( pData . text . c_str ( ) + tIndex , pData . text . length ( ) - tIndex ) ;
a . pop ( ) ;
2025-11-04 10:58:26 +03:00
break ;
2025-11-04 17:31:23 +03:00
}
else {
auto & nextStackCommand = * nextStackCommandIt ;
2025-11-04 10:58:26 +03:00
2025-11-04 17:31:23 +03:00
// Print with previous style
2025-11-04 10:58:26 +03:00
a . pushStyle ( tStyle ) ;
2025-11-04 17:31:23 +03:00
a . addText ( pData . text . c_str ( ) + tIndex , nextStackCommand . first . fTextByteIndex - tIndex ) ;
2025-11-04 10:58:26 +03:00
a . pop ( ) ;
2025-11-04 17:31:23 +03:00
tIndex = nextStackCommand . first . fTextByteIndex ;
// Modify stack with new data
for ( const std : : shared_ptr < TextStyleModifier > & modifier : nextStackCommand . second ) {
auto & modStack = modifierStacks [ modifier - > get_type ( ) ] ;
2025-11-04 19:04:09 +03:00
auto foundInStackIt = std : : find ( modStack . begin ( ) , modStack . end ( ) , modifier ) ;
if ( foundInStackIt = = modStack . end ( ) )
modStack . emplace_back ( modifier ) ;
2025-11-04 17:31:23 +03:00
else
2025-11-04 19:04:09 +03:00
modStack . erase ( foundInStackIt ) ;
2025-11-04 17:31:23 +03:00
}
// Modify style with new stacks
tStyle = initialTStyle ;
for ( auto & [ modType , modifier ] : modifierStacks ) {
if ( ! modifier . empty ( ) )
2025-11-04 19:04:09 +03:00
modifier . back ( ) - > modify_text_style ( tStyle ) ;
2025-11-04 17:31:23 +03:00
}
2025-11-04 10:58:26 +03:00
2025-11-04 17:31:23 +03:00
+ + nextStackCommandIt ;
}
2025-11-04 10:58:26 +03:00
}
2025-10-19 15:54:00 +03:00
pData . p = a . Build ( ) ;
pData . p - > layout ( width ) ;
pData . heightOffset = heightOffset ;
heightOffset + = pData . p - > getHeight ( ) ;
}
2025-11-04 17:31:23 +03:00
2025-10-18 13:32:12 +03:00
needsRebuild = false ;
2025-10-16 21:22:08 +03:00
}
}
2025-10-20 17:30:36 +03:00
void RichTextBox : : set_tab_space_width ( unsigned newTabWidth ) {
tabWidth = newTabWidth ;
}
2025-10-19 15:54:00 +03:00
RichTextBox : : TextPosition RichTextBox : : insert ( TextPosition pos , std : : string_view textToInsert ) {
pos = move ( Movement : : NOWHERE , pos ) ;
for ( char c : textToInsert ) {
if ( c = = ' \n ' ) {
2025-10-20 17:30:36 +03:00
if ( newlinesAllowed ) {
2025-11-04 17:31:23 +03:00
for ( TextStyleRangeModifier & tStyle : tStyleModifiers ) {
tStyle . apply_to_start_and_end ( [ & pos ] ( TextPosition & p ) {
if ( p . fParagraphIndex > pos . fParagraphIndex )
p . fParagraphIndex + + ;
else if ( p . fParagraphIndex = = pos . fParagraphIndex & & p . fTextByteIndex > = pos . fTextByteIndex ) {
p . fTextByteIndex = p . fTextByteIndex - pos . fTextByteIndex ;
p . fParagraphIndex + + ;
}
} ) ;
}
2025-10-20 17:30:36 +03:00
paragraphs . insert ( paragraphs . begin ( ) + pos . fParagraphIndex + 1 , ParagraphData { } ) ;
paragraphs [ pos . fParagraphIndex + 1 ] . text = paragraphs [ pos . fParagraphIndex ] . text . substr ( pos . fTextByteIndex , paragraphs [ pos . fParagraphIndex ] . text . size ( ) - pos . fTextByteIndex ) ;
paragraphs [ pos . fParagraphIndex ] . text . erase ( pos . fTextByteIndex , paragraphs [ pos . fParagraphIndex ] . text . size ( ) - pos . fTextByteIndex ) ;
pos . fParagraphIndex + + ;
pos . fTextByteIndex = 0 ;
}
}
else if ( c = = ' \t ' ) {
2025-11-04 17:31:23 +03:00
for ( TextStyleRangeModifier & tStyle : tStyleModifiers ) {
tStyle . apply_to_start_and_end ( [ & pos , tabWidth = tabWidth ] ( TextPosition & p ) {
if ( p . fParagraphIndex = = pos . fParagraphIndex & & p . fTextByteIndex > = pos . fTextByteIndex )
p . fTextByteIndex + = tabWidth ;
} ) ;
}
2025-10-20 17:30:36 +03:00
paragraphs [ pos . fParagraphIndex ] . text . insert ( pos . fTextByteIndex , std : : string ( tabWidth , ' ' ) ) ;
pos . fTextByteIndex + = tabWidth ;
2025-10-19 15:54:00 +03:00
}
else {
2025-11-04 17:31:23 +03:00
for ( TextStyleRangeModifier & tStyle : tStyleModifiers ) {
tStyle . apply_to_start_and_end ( [ & pos ] ( TextPosition & p ) {
if ( p . fParagraphIndex = = pos . fParagraphIndex & & p . fTextByteIndex > = pos . fTextByteIndex )
p . fTextByteIndex + + ;
} ) ;
}
2025-10-19 15:54:00 +03:00
paragraphs [ pos . fParagraphIndex ] . text . insert ( paragraphs [ pos . fParagraphIndex ] . text . begin ( ) + pos . fTextByteIndex , c ) ;
pos . fTextByteIndex + + ;
}
}
2025-11-04 17:31:23 +03:00
fit_text_style_ranges_in_text ( ) ;
2025-10-16 21:22:08 +03:00
needsRebuild = true ;
2025-10-19 15:54:00 +03:00
return pos ;
2025-10-16 21:22:08 +03:00
}
2025-10-19 15:54:00 +03:00
RichTextBox : : TextPosition RichTextBox : : remove ( TextPosition p1 , TextPosition p2 ) {
p1 = move ( Movement : : NOWHERE , p1 ) ;
p2 = move ( Movement : : NOWHERE , p2 ) ;
TextPosition start = std : : min ( p1 , p2 ) ;
TextPosition end = std : : max ( p1 , p2 ) ;
if ( start = = end | | start . fParagraphIndex = = paragraphs . size ( ) )
return start ;
if ( start . fParagraphIndex = = end . fParagraphIndex ) {
assert ( end . fTextByteIndex > start . fTextByteIndex ) ;
paragraphs [ start . fParagraphIndex ] . text . erase ( start . fTextByteIndex , end . fTextByteIndex - start . fTextByteIndex ) ;
}
else {
assert ( end . fParagraphIndex < paragraphs . size ( ) ) ;
auto & paragraph = paragraphs [ start . fParagraphIndex ] ;
paragraph . text . erase ( start . fTextByteIndex , paragraph . text . size ( ) - start . fTextByteIndex ) ;
paragraph . text . insert ( start . fTextByteIndex , paragraphs [ end . fParagraphIndex ] . text . substr ( end . fTextByteIndex ) ) ;
paragraphs . erase ( paragraphs . begin ( ) + start . fParagraphIndex + 1 , paragraphs . begin ( ) + end . fParagraphIndex + 1 ) ;
}
2025-11-04 17:31:23 +03:00
for ( TextStyleRangeModifier & tStyle : tStyleModifiers ) {
tStyle . apply_to_start_and_end ( [ & start , & end ] ( TextPosition & p ) {
if ( p > start ) {
if ( end > p )
p = start ;
else if ( start . fParagraphIndex = = p . fParagraphIndex )
p . fTextByteIndex - = ( end . fTextByteIndex - start . fTextByteIndex ) ;
else if ( end . fParagraphIndex = = p . fParagraphIndex ) {
p . fParagraphIndex = start . fParagraphIndex ;
p . fTextByteIndex = start . fTextByteIndex + ( p . fTextByteIndex - end . fTextByteIndex ) ;
}
else
p . fParagraphIndex - = ( end . fParagraphIndex - start . fParagraphIndex ) ;
}
} ) ;
}
fit_text_style_ranges_in_text ( ) ;
2025-10-16 21:22:08 +03:00
needsRebuild = true ;
2025-10-19 15:54:00 +03:00
return start ;
2025-10-16 21:22:08 +03:00
}
2025-10-18 13:32:12 +03:00
SkRect RichTextBox : : get_cursor_rect ( TextPosition pos ) {
2025-10-19 15:54:00 +03:00
pos = move ( Movement : : NOWHERE , pos ) ;
2025-10-20 11:41:22 +03:00
SkPoint topPoint ;
2025-10-16 21:22:08 +03:00
float height ;
2025-10-19 15:54:00 +03:00
const ParagraphData & pData = paragraphs [ pos . fParagraphIndex ] ;
if ( pData . text . empty ( ) ) {
2025-10-20 11:41:22 +03:00
bool isAlignLeft = ( pData . pStyle . getTextAlign ( ) = = skia : : textlayout : : TextAlign : : kLeft ) | | ( pData . pStyle . getTextAlign ( ) = = skia : : textlayout : : TextAlign : : kJustify & & pData . pStyle . getTextDirection ( ) = = skia : : textlayout : : TextDirection : : kLtr ) ;
topPoint = { isAlignLeft ? 0.0f : pData . p - > getMaxWidth ( ) , pData . heightOffset } ;
2025-10-19 15:54:00 +03:00
height = pData . p - > getHeight ( ) ;
2025-10-16 21:22:08 +03:00
}
2025-10-18 13:32:12 +03:00
else {
2025-10-19 15:54:00 +03:00
if ( pos . fTextByteIndex = = pData . text . size ( ) ) {
2025-10-18 13:32:12 +03:00
skia : : textlayout : : Paragraph : : GlyphClusterInfo glyphInfo ;
2025-10-19 15:54:00 +03:00
bool exists = pData . p - > getGlyphClusterAt ( prev_grapheme ( pData . text , pos . fTextByteIndex ) , & glyphInfo ) ;
2025-10-18 13:32:12 +03:00
if ( exists ) {
2025-10-20 11:41:22 +03:00
bool isLtr = glyphInfo . fGlyphClusterPosition = = skia : : textlayout : : TextDirection : : kLtr ;
topPoint = ( isLtr ? glyphInfo . fBounds . TR ( ) : glyphInfo . fBounds . TL ( ) ) + SkPoint { 0.0f , pData . heightOffset } ;
2025-10-18 13:32:12 +03:00
height = glyphInfo . fBounds . height ( ) ;
}
else {
2025-10-20 11:41:22 +03:00
topPoint = { 0.0f , pData . heightOffset } ;
2025-10-18 13:32:12 +03:00
height = 0.0f ;
}
2025-10-16 21:22:08 +03:00
}
else {
2025-10-18 13:32:12 +03:00
skia : : textlayout : : Paragraph : : GlyphClusterInfo glyphInfo ;
2025-10-19 15:54:00 +03:00
bool exists = pData . p - > getGlyphClusterAt ( pos . fTextByteIndex , & glyphInfo ) ;
2025-10-18 13:32:12 +03:00
if ( exists ) {
2025-10-20 11:41:22 +03:00
bool isLtr = glyphInfo . fGlyphClusterPosition = = skia : : textlayout : : TextDirection : : kLtr ;
topPoint = ( isLtr ? glyphInfo . fBounds . TL ( ) : glyphInfo . fBounds . TR ( ) ) + SkPoint { 0.0f , pData . heightOffset } ;
2025-10-18 13:32:12 +03:00
height = glyphInfo . fBounds . height ( ) ;
}
else {
2025-10-20 11:41:22 +03:00
topPoint = { 0.0f , pData . heightOffset } ;
2025-10-18 13:32:12 +03:00
height = 0.0f ;
}
2025-10-16 21:22:08 +03:00
}
}
2025-10-20 11:41:22 +03:00
constexpr float CURSOR_WIDTH = 3.0f ;
return SkRect : : MakeXYWH ( topPoint . x ( ) - CURSOR_WIDTH * 0.5f , topPoint . y ( ) , CURSOR_WIDTH , height ) ;
2025-10-18 13:32:12 +03:00
}
2025-10-19 15:54:00 +03:00
void RichTextBox : : rects_between_text_positions_func ( TextPosition p1 , TextPosition p2 , std : : function < void ( const SkRect & r ) > f ) {
p1 = move ( Movement : : NOWHERE , p1 ) ;
p2 = move ( Movement : : NOWHERE , p2 ) ;
2025-10-18 13:32:12 +03:00
2025-10-20 11:41:22 +03:00
constexpr float SELECTION_RECT_EXTRA_AREA = 1.0f ;
2025-10-18 13:32:12 +03:00
2025-10-19 15:54:00 +03:00
TextPosition start = std : : min ( p1 , p2 ) ;
TextPosition end = std : : max ( p1 , p2 ) ;
2025-10-18 13:32:12 +03:00
2025-10-19 15:54:00 +03:00
for ( size_t p = start . fParagraphIndex ; p < = end . fParagraphIndex ; p + + ) {
size_t tStart = p = = start . fParagraphIndex ? start . fTextByteIndex : 0 ;
size_t tEnd = p = = end . fParagraphIndex ? end . fTextByteIndex : paragraphs [ p ] . text . size ( ) ;
ParagraphData & pData = paragraphs [ p ] ;
for ( size_t t = tStart ; t < tEnd ; t + + ) {
2025-10-18 13:32:12 +03:00
skia : : textlayout : : Paragraph : : GlyphClusterInfo glyphInfo ;
2025-10-19 15:54:00 +03:00
if ( pData . p - > getGlyphClusterAt ( t , & glyphInfo ) ) {
int lineNumber = pData . p - > getLineNumberAt ( t ) ;
2025-10-18 13:32:12 +03:00
if ( lineNumber = = - 1 )
break ;
skia : : textlayout : : LineMetrics lineMetrics ;
2025-10-19 15:54:00 +03:00
if ( pData . p - > getLineMetricsAt ( lineNumber , & lineMetrics ) ) {
SkRect r = SkRect : : MakeLTRB ( glyphInfo . fBounds . left ( ) - SELECTION_RECT_EXTRA_AREA , lineMetrics . fBaseline - lineMetrics . fAscent - SELECTION_RECT_EXTRA_AREA + pData . heightOffset , glyphInfo . fBounds . right ( ) + SELECTION_RECT_EXTRA_AREA , lineMetrics . fBaseline + lineMetrics . fDescent + pData . heightOffset + SELECTION_RECT_EXTRA_AREA ) ;
2025-10-18 13:32:12 +03:00
f ( r ) ;
}
}
}
}
}
2025-10-16 21:22:08 +03:00
void RichTextBox : : paint ( SkCanvas * canvas , const PaintOpts & paintOpts ) {
rebuild ( ) ;
if ( paintOpts . cursor . has_value ( ) ) {
auto & cur = paintOpts . cursor . value ( ) ;
2025-10-18 13:32:12 +03:00
SkPaint p { SkColor4f { paintOpts . cursorColor . x ( ) , paintOpts . cursorColor . y ( ) , paintOpts . cursorColor . z ( ) , 1.0f } } ;
if ( cur . selectionBeginPos ! = cur . selectionEndPos ) {
canvas - > saveLayerAlphaf ( nullptr , 0.6f ) ;
rects_between_text_positions_func ( cur . selectionBeginPos , cur . selectionEndPos , [ & ] ( const SkRect & rect ) {
canvas - > drawRect ( rect , p ) ;
} ) ;
canvas - > restore ( ) ;
2025-10-16 21:22:08 +03:00
}
2025-10-19 15:54:00 +03:00
canvas - > drawRect ( get_cursor_rect ( cur . pos ) , p ) ;
2025-10-16 21:22:08 +03:00
}
2025-10-19 15:54:00 +03:00
for ( auto & pData : paragraphs )
pData . p - > paint ( canvas , 0.0f , pData . heightOffset ) ;
2025-10-16 21:22:08 +03:00
}