From bc071bd88d5b7c7ef1bae94dc89e2ee3aca2d5c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=ADn=20Lucas=20Golini?= Date: Sat, 9 May 2026 01:38:43 -0300 Subject: [PATCH] Add `line-height` and `text-indent` support. --- .../assets/html/eepp-ui-anchor-margins.webp | Bin 1476 -> 1478 bytes .../assets/html/eepp-ui-span-padding.webp | Bin 6372 -> 6310 bytes include/eepp/graphics/richtext.hpp | 15 +++++ include/eepp/ui/css/propertydefinition.hpp | 2 + include/eepp/ui/uirichtext.hpp | 10 ++++ src/eepp/graphics/richtext.cpp | 26 ++++++-- src/eepp/ui/css/stylesheetspecification.cpp | 2 + src/eepp/ui/uirichtext.cpp | 56 +++++++++++++++++- src/examples/richtext/richtext.cpp | 2 + 9 files changed, 108 insertions(+), 5 deletions(-) diff --git a/bin/unit_tests/assets/html/eepp-ui-anchor-margins.webp b/bin/unit_tests/assets/html/eepp-ui-anchor-margins.webp index ac9873391103a4d7d18c4d31b1bda85561361467..f501815a6c2867f5ae6417084de4dcf38cccf954 100644 GIT binary patch delta 1227 zcmX@YeT-W$$kWYjA1ebxxT{k@Sb&Ak#)*PE>UVE@*|TKj#ar8QZv)Zp$lKf2Zk+t? z%(Z{J{Z>sfx0Sc?cloclv-gQd(*3kpmnXZ-1A31$|36s2wf#$W_|N0h-v6C6b>__N zEruyYpZ0Ob?}+@pNPo_fn@aNCGdK5bGu;>e!$xq%uY-TLYORsCo?O1@$(jRUOD{JE zF5cBvc-L!Iy1%@dPOL!U!zW!eI&}tBp69kd6TKR-&m{5jlqYjmOnT$=_*k!P?mDZR z(!re-hn>IQ+;Dtjcx>mbnEvOvex`dDPg-}pIp}#<^rE+C_8jC^jl2Kl$L>kFH|4)4 z$1{JstmcyS$oc%8V&2Ic7^N9oC!b+dt{3X=xnB3dyt(6$+&tafS#j@Al|7BR)wyMj z(dIK5KI`;mov03ts_1`SbhGEKjp@PIEse>a%|9_N@~%&R|7>HM{pFWlyV|su={?-* z(){z_igT+zl`2ok`FQ;Cc2n~Wvo}v$vbt;e`=_m&_U>;qyXK-)_B{W?v&8a+-&tLX zbq!HPyv%+CsJ(FUxaT?{++Io?Q5L_O)2+x!3;3)qJblcB10#rPEUc z-hGf;do#BBS=_cS!Y5H2ct_eKW5V%$e!M?-h__t&DYmpa^3;r(*QNjY_y=C{{dsQ9 zpXn=hc`iw@4z9~nko1`Sb87#JbpgtD3+uN&pZzOBSLof3=MQi1(^r_4UHL|ONh#m< z6+##P+h(pXln?H^JUM-D>;oQs%LS#^{g!_%&kI=0@_6gx@D5;@#l>IT`EKh4f0fOr z&J?*N7nQ~fzm1zZZ@Tv$Wm(lE@9MjCW*HY2d;^A)?$nZ>_Q(0J&RxFj(~YQRLGwzH znKm=)`DIK$hGrd8-+Cs8wWgZYcgg~j>FbZ%8g1W~5io6q&x6)eg+cZ6|6PCkPg73! z^o!+}e_3B&$rzoo=iBS~FFAibJiKQe*m}0qzJ{2)*In+d7CN#i{q6HiN4?|IeC{OH zq^idLd;YPy#>>9vrT>(##RtOdO>eJT`MGjuV|1}veQnKpwQcEdA6IVLtNtLi>F4a% z?~Xn{H$$ViJ^G5j^{kmU#{?^I5pSMo`Ftd>R z_{qbwcSM^hUv#pcct*?E{n5!Onv0LT`lt3OZmNQ=zEz}jQ1h035zp?JE?Y8tV)VA} zP3f)+_jYAm(tm!+SYO`s_T(M4Yg-#MY;lZuc8P^>cJ=j4dH_f=~L6+d#s<}n`I;Sga6;^{U`rR^{X#E_PFu?1ACxAGwD=4xf-dYj~n9d JSJylk8309ot49C; delta 1225 zcmX@ceS}*u$kWYj4=V#hxT{k@Sb&AkhKYhZ>Thp*o3o_ym~r;)ZMi^{3M7vC-8y6U z`~55%|4++4Hy$r^{~|x>wnv}Jx7=&4F6*k9O)G3Z$p5)s;jzDLYkkhod-{*Y$Z%r zWlLONo3Z5Qm&8MtSOvxW)}1*1h<)*Nk(%td;NGX+!e)M+=Q^`a>OFb(tm;-w=_m7L zLLb}O-)&CpPhP)EXzNZn{abUgs@zp$``M?STf5EW_RPv=aj!l5UVbcBy}il)-SK^F zZCHM(eKo40|9R2Xp1U@t2V=K1CVw{n#JI@2KK=c(jcxXqUwZ9&BN{yYL)@Z+ ze-c8{L%;44^xF30@yFYeEs{7LAT~d!Z}TkccRwGb#Qa`XdfjjN*Zt!2_s-Wfirf9kNVUa! zk9&66^?5s|>;82yT75>xSZea>ZnaNwTgAS!@k%|}xIWlS{(Dn^)%`^!`re|(pQ{ef zf2rxUOY{EBssDvS%{!;$q^~;9tHPXVy#JHnRnb!qg`?`L_pR)|s`Rk0UT)u8u%nLe zbBuak(5)r-_x|LzqMtL0=FGEs{gx@c;wai&s?Qs;;PYeZK3u zll{aqTE?d(drw}i_1#i^wJL7ihrW_29lCEzf8Si-*nCU+pU?J?OQQc}>_f`u%{rBw z&FgmONnpyS6ubI@D@VdtO3uGxv|04STTzq1rb{vV^?~%rPN``UguJxUJ!gV`R&)+(K zxhbYkcjk`6KTdh=dUXEr_a7m35|`&|yno)Xwk~U1Z1D5XMa-f!j7$2@PZ`VWnckkfqjqg;~rE?Tgyi z-PhJhRriTsJ)-vYc-ZsM=kGn%PxO_tk^8~F?Z^EGAD5Vbvn>1ngZ03iOFC6gu0|^9 M;{>}q{Jb730O%~68vpB%{6 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 literal 6372 zcmd6q_ct6s*TTp0sapqkmFH^blRTcU?F-d6{*U$7 zHx4cD@89hBWu2Qiesdlhe@pj3P;R&U+4jfY1&bBWFhu?{RPTb!(EI{aals2h#*^5{ z6javEw94r*)eEUA$%}NKiWq3lE1MPT!1fe|=f#|nQ~?K^0KGNG8^`ses@9k z2&M)ks|FSplpw~6`-_YO8~KYOf|DXI1?kG@xZqp+lUm`5>-+mWWZQK=p0%r>*k=ly zn{!%nW@?@1y6f)jO;jqKjcPK-#jba&NPen_sC6v-Ge}(HSb%Z@J5LgOzs+f2rg+Pf zA1o8U@6uG%OZx}@8Q8+scJQ%~kkIYHo&Ul0qIdg8eRbmenVyaOSn=Y`7;ti&)#nG= z7-~|!ASUl}IVb$Y02aEg0Xu`LzDTY{uGF-gKv?jHs;3^&^}fcvYlYcrEO~2nfq^Z* zX3H6A8XOM{SZF2H*16v4AkyIlF`=f+RYbG6NXpLsx)5+$lswZb4-0eX6vEu^0+L^ho-2($t3#S)WZJ1LZjeR|7^g@}jbT zPfN$DN1)M_tW;^k1h%fL4Cr}abDG9@-j>WaMpaEOLR>*Z1+;^*TK4jcB$}{#+-S#;qrmG znNY=#u&r##sPRcyU+h_rExFSE=NZOI@t8jkXXUO)@x)(WciL*p%kMR12+V`gW2+~I z9G8_b#l^=Gu{pwBx0O`dM1}1@qB?Dd=T@vo?dT0@|0lEZn=^Cf!Pxi_l^MkzrGPQ0 zZ{+%e4@G-=>~(6|dzDS?`r8TAhC17-LUHkbMv^~&k%RP;(C@x4n&p?%cIP{VQM8p6 zqC7f-84VJ>FIqgXJ54&$i)nr^Ip?>H_+?C$!_DaM8GYFuE!zN>DNt{)vPONT*DRtrqbhZrJ%mZI86d>xC00I1bA^ zj@4ZzoD%Izs0SL$mIF=kf%ww-6ctpVYyH*vGD=KPWX-R30sn#7m}=jN1``Haw6j&c zcA{-xXd~~a6n~y;%_}dhGV}r^zDuF0^O)q5vE+kdZawuWMrEDiNH6t8Gg*-n&<|UM z^s>T-+zL&lv{uch>uM|3^H}Ojf(ltB0_HJZQs5U&z-QF)TjqFHCDHERAJ_9#vwmGB zUpYgpa>P`0wXM(qrSAa>@nmGoD`_G9?7Sf`99og1U6GmvkyU?erUr}&kGO=d1Jay3T@M@)(n^Kx-kn3>n!sC(QBV$2pTa*Tzj_BNSwUSQk)dz`A7ivaa~VJ63)JJR zr~ZH_Gvp#SkgNV>*_3w_9Nlt7mG#&2-GH&^I6-fzp46g_-?J{f-xd+9isDhj@25eG zZ(kk5>$K|q)tVRWZexy<_8P z0NVzO_vJh!-n8w6gczqW0*SVwCQp;DCc*{CNO%;;$oZkZ2Ty~5&#+DR1-sQbH$`dV zVJSL?R!4=pFSuMm-jU__snnVRbTF}C=K_;St4aP4dFdQzp+I5>SJQ`2d@YAwX)FYc@@5@|JV|6?M`}D{HL6G` zdpD()pvbt!=;34{gxpbSywnB{AQbN#*(r63g zAm_I>6MlSEolm&l`~F$9wkBg~0IkJ&i#Bh&8z3r`1PX%9ZdV4;|tFGzT5DAC5txG)lqhF^<2fDvFu1q2K*Dxq7(MRVkG!TF@^<&LZ{;8Zjemk~*Kv+YKPKnL1BT9Dt~L+x-bT69z{HN3lMQc7I19bZ`*D*9{DNv&`Fjh|R67xrQHP z1YCPU1_jrCvyG^mgYxgoBkvgH&iwVW-s!@|Ngw=T^4Ye5Eu_c>NHW{pzA79FUxC-A zfRB7%QEp6W=`LyLA6YjZNg!J##d8Bh^BmsB(w>X#tB=JS;w1PXX z>SC)gP4qZLO3TZZ%c}Y~@hh3mqQ^>aHyI4)h>Qu?aX&MbQx!6fYq^l8%)B1gTcZld zGQu90eEPJEp>VJs%kDy&il^`>7*mRJ{}4ge$5hDhCv9oMHBRs8gx1;bL&Y<(_SUHu z%8uK=t^`vb{EE!$1tmg0l!~i0B^AXPYNX1izGM;E|2f~B+HrX~?g{fg=N*oL-UJve zbWgwKq%E~~#ls^expeupBjThn_)9W6g8w6DpoKkUvd@w~H;G@$peORlLPm4gCmxHd zrV)j#ITcuw84WgK*{@C9&hG1al_7FcSLj{bf`UucVnKUCzqXYVOsuxera}C3h8qmoz=+2orWi7CoT2^6d4OFiZlZXdsRK3NnK9j z_(N^%BbEmHrevbTq*M5t&^W?TqA}th#@FOt#STLua3J?XXMrhV!|jscXhxYGNAe!?*kOA75e!-NoZcTRr@ewYK2jk* z6s7xQ%m&c>L^-e{iJLR{H@DGo+oHaUS*&n#&uR={<7^5K6nOyL$sTd)vq9P{2&l$_ zIEg)mrN2QwC6^>j>knrlip{C5Cb{OwUsMh(qIgMuC2fjuTMJtV@pp=RHbK02uvchK z&LDUdpi7!ko3?{jD&%qjdh!<5!TO&I>G>gouEldRaqDP*q_C|?5^YQ$kt#K>$efNV z_p{1P=2DiJ87V*_-_KNt3)eyz=#z)$Gg~Vds9jer(Rr=ygq););nF&mr(rH+VLNJ; zen798Bx6C3L~E^N1)-@ZtZi4R}uS*T5Grlyu$u@ zdtW-&V=tvMjsX)!!r6NPU)IH5;Is^BJAHBMB{EU+K%gwQBGn`#Py=nka5Sw_{d=R$ z=yH&CR)sM7#E$#v)}RNar|pO`=d1+Ughh_fO`oz$;tu_`#XAVvcFSrQt>s%_+?8}? zD%~BlxP=oaA36<(*7fih6R7CAk;D>~-m`~!X=d4}*uDMU96HQzZ+fA`K)X5C+acze zJ{$+)Ji>`*Xvjz?H1^YSGJH9Yb)Pip>t6c#Eb_e582(m~hTt(RQ5X9qrtwyJCj-j9Hs0*;{D%gp{7fV`XbW zJcsR=HwMel0`3q#14xG+MJs4v9DaYjSm}u0eTA(ygfw1M8Zh<&k(avhM zu|Gxlo+@*z=4I}sVC{@wSxOEc28H~ub2nSEvi?smu32O|0ZkldYbc>m>46d;~FM=3nx`o>m|7%O^Tn+TT9liA8XD#fckJmK;jbQ%sV zw+;mxQZ$0>erG?oRe3{4TrLYNNmv(|U*-VE_S_H~8E z6!FwA8~^`?QZ!$I%pnHx0pCF{zZu zhP$F$O>ed`s8;1d{*}jwrM%gIiR*Qa4T=HPDb#-9m5|I!*s?rEmE zcqzw%AZnRKAv*Etnt$cZ8~aGk7Qf^hxHM)b<{)F)?_t)#r=gJN!WYG{Zk7=NgL@4{ zJxV)Ch5x-__S%wB_1}LD@_&^7 zwc}$S#vQ3hd&9+3=;x=C0l$UQDPMlRJ*uU7j91*mJZozuuSZ`V{qLx>t z$>fFEXnQ5gMA_FPhV?7x5aWWLi$Pst5lb{?WI})fl({sL;w?;uXt{w4YpAaV#?l<8 zF%evWPKYb0j}WcPpz0&E);ipNT3+RJEV6jI2hzfIk<2qo+W@xD$yTq4rm24OMRt|WcjN{~qij&0N-dIBsB5!1?ctdG##CTQa@jsSCH_W_ibvq};8KSU?K z7@E^Kel70q5EV0(XRF34^TSEl-z?0O-1ERG1`*Oh7eQpZ5cw)#od4+V)ps-(?tzPA zNL75jhI`_J0ais)s%a61UzmCI@pYS4bi^`*`Ds zTZ}34MqM&;3a7jHn+Tb(<*0(*a%1(1;w<3-rO1;aAH+865G9~Q(y^?e7x~b1lxlt$ z`3oByuER0iRbC@qlj{O4Ek>u{wU^A*i)UqZBb20um?_C1dlr8>J>E|lwQMZYXc_I) zkLO49%<1Z(4Ne3K(gJ-}m}Uh;Q&x{2LPZ}W>9aM$+G=aD&KT=m^y5n(q^oxn5Lyir+;rgWQ0;o){^&xY9`%gP1lC>9^W(Ba zv~4BxBnc!yEFVR4|9j7bDd;j8Cmm$l#f){i`LW4p{}KF;OW$0$uOBTSzUL;Khvv5t z;s&ze#w1@3N{M~&g_)AH$<1Z92+Y*_q?xKjRNIn`Y{evR*Dg$)kZnqk8*=(iiFBH) zW1{^K9|7-$?+?xPpv#$jbX)-SV)0s0!w^`~PLg86QLPssdFoX-|5sxl6j+U2|Hr+4 z;yq-7d=DWC$^mF+US1xZzQB zM&ihPfwHH$L16EtA5w?~jq9Q+{}Auxx*!XAiIp7%PWQEK8upc_9Ty6! zKof9w`}t5HirpXc4n)`6Ovs&9{y2!Xvo<~N>0VqorhW9~s_$2=bZ4VSA6nsN#jl1O zfJ-PQCfSkBgt&efX=`$pWSfeTCvB?^%YIIo8~qK-kb>?0(TD{9BoV73Tt#5#jFO>L z>PhMT;Y*ix;D!H?S}{l&cwrNTTBLa!OaVJL5KKiK=uBAV7<}oR4dBf)Tb^uF({xR=APaR#{=5^?|A4F6mFKi%>FuLX@& getLines() const { return mLines; } + /** Sets line-height as a multiplier of the font's default line spacing. + * Use 0 to reset to font default. */ + void setLineHeight( Float height ); + + /** @return The line height multiplier. */ + Float getLineHeight() const { return mLineHeight; } + + /** Sets text-indent (pixels) for the first line. */ + void setTextIndent( Float indent ); + + /** @return The text indent in pixels. */ + Float getTextIndent() const { return mTextIndent; } + /** @brief Sets the text selection range. */ void setSelection( TextSelectionRange range ); @@ -207,6 +220,8 @@ class EE_API RichText : public Drawable { Sizef mSize; Int64 mTotalCharacterCount{ 0 }; bool mNeedsLayoutUpdate{ true }; + Float mLineHeight{ 0 }; + Float mTextIndent{ 0 }; }; }} // namespace EE::Graphics diff --git a/include/eepp/ui/css/propertydefinition.hpp b/include/eepp/ui/css/propertydefinition.hpp index f60eddee0..a82a3a822 100644 --- a/include/eepp/ui/css/propertydefinition.hpp +++ b/include/eepp/ui/css/propertydefinition.hpp @@ -220,6 +220,8 @@ enum class PropertyId : Uint32 { TextAsFallback = String::hash( "text-as-fallback" ), SelectOnClick = String::hash( "select-on-click" ), LineSpacing = String::hash( "line-spacing" ), + LineHeight = String::hash( "line-height" ), + TextIndent = String::hash( "text-indent" ), GravityOwner = String::hash( "gravity-owner" ), Href = String::hash( "href" ), Focusable = String::hash( "focusable" ), diff --git a/include/eepp/ui/uirichtext.hpp b/include/eepp/ui/uirichtext.hpp index 59e6b07d4..1302d66a3 100644 --- a/include/eepp/ui/uirichtext.hpp +++ b/include/eepp/ui/uirichtext.hpp @@ -113,6 +113,14 @@ class EE_API UIRichText : public UIHTMLWidget { UIRichText* setTextAlign( const Uint32& align ); + Float getLineHeightPx() const { return mLineHeightPx; } + + UIRichText* setLineHeightPx( Float height ); + + Float getTextIndentPx() const { return mTextIndentPx; } + + UIRichText* setTextIndentPx( Float indent ); + bool isTextSelectionEnabled() const; void setTextSelectionEnabled( bool active ); @@ -138,6 +146,8 @@ class EE_API UIRichText : public UIHTMLWidget { Int64 mSelCurInit{ 0 }; Int64 mSelCurEnd{ 0 }; bool mSelecting{ false }; + Float mLineHeightPx{ 0 }; + Float mTextIndentPx{ 0 }; explicit UIRichText( const std::string& tag = "richtext" ); diff --git a/src/eepp/graphics/richtext.cpp b/src/eepp/graphics/richtext.cpp index e0c458ffe..00a2873a3 100644 --- a/src/eepp/graphics/richtext.cpp +++ b/src/eepp/graphics/richtext.cpp @@ -130,6 +130,20 @@ void RichText::setSelection( TextSelectionRange range ) { } } +void RichText::setLineHeight( Float height ) { + if ( mLineHeight != height ) { + mLineHeight = height; + invalidate(); + } +} + +void RichText::setTextIndent( Float indent ) { + if ( mTextIndent != indent ) { + mTextIndent = indent; + invalidate(); + } +} + void RichText::setSelectionColor( const Color& color ) { mSelectionColor = color; } @@ -466,7 +480,7 @@ void RichText::updateLayout() { mLines.clear(); mLines.push_back( RenderParagraph() ); - Float curX = 0; + Float curX = mTextIndent; Float maxWidth = 0; Int64 curCharIdx = 0; @@ -524,7 +538,9 @@ void RichText::updateLayout() { renderSpanText->setStyleConfig( fontStyle ); Float ascent = fontStyle.Font->getAscent( fontStyle.CharacterSize ); - Float height = fontStyle.Font->getLineSpacing( fontStyle.CharacterSize ); + Float height = mLineHeight > 0 + ? mLineHeight + : fontStyle.Font->getLineSpacing( fontStyle.CharacterSize ); Float spanWidth = renderSpanText->getTextWidth(); RenderSpan renderSpan; @@ -686,7 +702,7 @@ void RichText::updateLayout() { mLines.clear(); mLines.push_back( RenderParagraph() ); - Float curX = 0; + Float curX = mTextIndent; Float maxWidth = 0; Int64 curCharIdx = 0; @@ -805,7 +821,9 @@ void RichText::updateLayout() { renderSpanText->setStyleConfig( fontStyle ); Float ascent = fontStyle.Font->getAscent( fontStyle.CharacterSize ); - Float height = fontStyle.Font->getLineSpacing( fontStyle.CharacterSize ); + Float height = mLineHeight > 0 + ? mLineHeight + : fontStyle.Font->getLineSpacing( fontStyle.CharacterSize ); Float spanWidth = renderSpanText->getTextWidth(); RenderSpan renderSpan; diff --git a/src/eepp/ui/css/stylesheetspecification.cpp b/src/eepp/ui/css/stylesheetspecification.cpp index 8d322d526..9420e8900 100644 --- a/src/eepp/ui/css/stylesheetspecification.cpp +++ b/src/eepp/ui/css/stylesheetspecification.cpp @@ -229,6 +229,8 @@ void StyleSheetSpecification::registerDefaultProperties() { registerProperty( "font-style", "", true ).addAlias( "font-weight" ); registerProperty( "text-decoration", "", true ); registerProperty( "line-spacing", "", true ).setType( PropertyType::NumberLength ); + registerProperty( "line-height", "", true ).setType( PropertyType::NumberLength ); + registerProperty( "text-indent", "", true ).setType( PropertyType::NumberLength ); registerProperty( "text-stroke-width", "", true ) .setType( PropertyType::NumberLength ) .addAlias( "fontoutlinethickness" ); diff --git a/src/eepp/ui/uirichtext.cpp b/src/eepp/ui/uirichtext.cpp index 61550af58..9dde96bb3 100644 --- a/src/eepp/ui/uirichtext.cpp +++ b/src/eepp/ui/uirichtext.cpp @@ -316,6 +316,33 @@ bool UIRichText::applyProperty( const StyleSheetProperty& attribute ) { mDataProperties["data-language"] = attribute; break; } + case PropertyId::LineHeight: { + std::string val = attribute.value(); + if ( val == "normal" || val.empty() ) { + setLineHeightPx( 0 ); + } else { + // Unitless number: multiplier of font size + bool isUnitless = !val.empty(); + for ( char c : val ) { + if ( c != '-' && c != '+' && c != '.' && !String::isNumber( c, false ) ) { + isUnitless = false; + break; + } + } + + if ( isUnitless ) { + Float multiplier = StyleSheetLength::fromString( val, 0 ).getValue(); + setLineHeightPx( multiplier * getFontSize() ); + } else { + setLineHeightPx( lengthFromValue( attribute ) ); + } + } + break; + } + case PropertyId::TextIndent: { + setTextIndentPx( lengthFromValue( attribute ) ); + break; + } default: return UIHTMLWidget::applyProperty( attribute ); } @@ -360,6 +387,10 @@ std::string UIRichText::getPropertyString( const PropertyDefinition* propertyDef return getTextAlign() == TEXT_ALIGN_CENTER ? "center" : ( getTextAlign() == TEXT_ALIGN_RIGHT ? "right" : "left" ); + case PropertyId::LineHeight: + return mLineHeightPx > 0 ? String::fromFloat( mLineHeightPx, "px" ) : "normal"; + case PropertyId::TextIndent: + return mTextIndentPx > 0 ? String::fromFloat( mTextIndentPx, "px" ) : "0"; default: return UIHTMLWidget::getPropertyString( propertyDef, propertyIndex ); } @@ -372,7 +403,7 @@ std::vector UIRichText::getPropertiesImplemented() const { PropertyId::Color, PropertyId::TextShadowColor, PropertyId::TextShadowOffset, PropertyId::TextStrokeWidth, PropertyId::TextStrokeColor, PropertyId::TextAlign, PropertyId::SelectionColor, PropertyId::SelectionBackColor, PropertyId::TextSelection, - PropertyId::TextDecoration }; + PropertyId::TextDecoration, PropertyId::LineHeight, PropertyId::TextIndent }; props.insert( props.end(), local.begin(), local.end() ); return props; } @@ -542,6 +573,24 @@ UIRichText* UIRichText::setTextAlign( const Uint32& align ) { return this; } +UIRichText* UIRichText::setLineHeightPx( Float height ) { + if ( mLineHeightPx != height ) { + mLineHeightPx = height; + notifyLayoutAttrChange(); + notifyLayoutAttrChangeParent(); + } + return this; +} + +UIRichText* UIRichText::setTextIndentPx( Float indent ) { + if ( mTextIndentPx != indent ) { + mTextIndentPx = indent; + notifyLayoutAttrChange(); + notifyLayoutAttrChangeParent(); + } + return this; +} + void UIRichText::loadFromXmlNode( const pugi::xml_node& node ) { beginAttributesTransaction(); @@ -657,6 +706,11 @@ String UIRichText::UIRichText::collapseInternalWhitespace( const String& s ) { void UIRichText::rebuildRichText( UILayout* container, RichText& richText, IntrinsicMode mode ) { richText.clear(); + if ( container->isType( UI_TYPE_RICHTEXT ) ) { + auto* uiRt = static_cast( container ); + richText.setLineHeight( uiRt->getLineHeightPx() ); + richText.setTextIndent( uiRt->getTextIndentPx() ); + } Float maxWidth = 0; if ( container->getLayoutWidthPolicy() == SizePolicy::WrapContent ) { maxWidth = container->getMatchParentWidth() - container->getPixelsContentOffset().Left - diff --git a/src/examples/richtext/richtext.cpp b/src/examples/richtext/richtext.cpp index ce3d00266..ccc0852c4 100644 --- a/src/examples/richtext/richtext.cpp +++ b/src/examples/richtext/richtext.cpp @@ -20,6 +20,8 @@ EE_MAIN_FUNC int main( int argc, char* argv[] ) { richText.getFontStyleConfig().Font = font; richText.getFontStyleConfig().CharacterSize = 24; richText.setAlign( TEXT_ALIGN_LEFT ); + richText.setLineHeight( 36 ); // Line height of 36px (1.5x 24px font) + richText.setTextIndent( 30 ); // Indent first line 30px // Add spans using the helper method richText.addSpan( "Hello " );