From 0802c2753282c64d6723facb45fa97b48241e598 Mon Sep 17 00:00:00 2001 From: Niv Baehr Date: Sun, 28 Dec 2025 01:14:31 +0200 Subject: [PATCH] Draw text line by line (#8379) --- CMake/Tests.cmake | 10 ++ Source/engine/render/text_render.cpp | 147 ++++++++++++++---- Source/engine/render/text_render.hpp | 2 + .../cursor-end.png | Bin 0 -> 1297 bytes .../cursor-middle.png | Bin 0 -> 1308 bytes .../cursor-start.png | Bin 0 -> 1286 bytes .../highlight-full.png | Bin 0 -> 1292 bytes .../highlight-partial.png | Bin 0 -> 1334 bytes .../multiline_cursor-end_first_line.png | Bin 0 -> 1510 bytes .../multiline_cursor-end_second_line.png | Bin 0 -> 1513 bytes .../multiline_cursor-middle_second_line.png | Bin 0 -> 1516 bytes .../multiline_cursor-start_second_line.png | Bin 0 -> 1504 bytes .../multiline_highlight.png | Bin 0 -> 1334 bytes test/text_render_integration_test.cpp | 110 +++++++++++++ 14 files changed, 241 insertions(+), 28 deletions(-) create mode 100644 test/fixtures/text_render_integration_test/cursor-end.png create mode 100644 test/fixtures/text_render_integration_test/cursor-middle.png create mode 100644 test/fixtures/text_render_integration_test/cursor-start.png create mode 100644 test/fixtures/text_render_integration_test/highlight-full.png create mode 100644 test/fixtures/text_render_integration_test/highlight-partial.png create mode 100644 test/fixtures/text_render_integration_test/multiline_cursor-end_first_line.png create mode 100644 test/fixtures/text_render_integration_test/multiline_cursor-end_second_line.png create mode 100644 test/fixtures/text_render_integration_test/multiline_cursor-middle_second_line.png create mode 100644 test/fixtures/text_render_integration_test/multiline_cursor-start_second_line.png create mode 100644 test/fixtures/text_render_integration_test/multiline_highlight.png diff --git a/CMake/Tests.cmake b/CMake/Tests.cmake index bb37b85b5..95328c02a 100644 --- a/CMake/Tests.cmake +++ b/CMake/Tests.cmake @@ -170,6 +170,16 @@ if(DEVILUTIONX_SCREENSHOT_FORMAT STREQUAL DEVILUTIONX_SCREENSHOT_FORMAT_PNG AND kerning_fit_spacing__align_right.png vertical_overflow.png vertical_overflow-colors.png + cursor-start.png + cursor-middle.png + cursor-end.png + multiline_cursor-end_first_line.png + multiline_cursor-start_second_line.png + multiline_cursor-middle_second_line.png + multiline_cursor-end_second_line.png + highlight-partial.png + highlight-full.png + multiline_highlight.png SRC_PREFIX test/fixtures/text_render_integration_test/ OUTPUT_DIR "${DEVILUTIONX_TEST_FIXTURES_OUTPUT_DIRECTORY}/text_render_integration_test" OUTPUT_VARIABLE _text_render_integration_test_fixtures diff --git a/Source/engine/render/text_render.cpp b/Source/engine/render/text_render.cpp index c2abebd31..dabb93a76 100644 --- a/Source/engine/render/text_render.cpp +++ b/Source/engine/render/text_render.cpp @@ -437,6 +437,88 @@ int GetLineStartX(UiFlags flags, const Rectangle &rect, int lineWidth) return rect.position.x; } +void DrawLine( + const Surface &out, + std::string_view text, + Point characterPosition, + Rectangle rect, + UiFlags flags, + int curSpacing, + GameFontTables size, + text_color color, + bool outline, + const TextRenderOptions &opts, + size_t lineStartPos, + int totalWidth) +{ + CurrentFont currentFont; + + std::string_view lineCopy = text; + + size_t currentPos = 0; + + size_t cpLen; + + const auto maybeDrawCursor = [&]() { + const auto byteIndex = static_cast(lineStartPos + currentPos); + Point position = characterPosition; + if (opts.cursorPosition == byteIndex) { + if (GetAnimationFrame(2, 500) != 0 || opts.cursorStatic) { + FontStack baseFont = LoadFont(size, color, 0); + if (baseFont.has_value()) { + DrawFont(out, position, baseFont.glyph('|'), color, outline); + } + } + if (opts.renderedCursorPositionOut != nullptr) { + *opts.renderedCursorPositionOut = position; + } + } + }; + + // Start from the beginning of the line + characterPosition.x = GetLineStartX(flags, rect, totalWidth); + + while (!lineCopy.empty()) { + char32_t c = DecodeFirstUtf8CodePoint(lineCopy, &cpLen); + if (c == Utf8DecodeError) break; + if (c == ZWSP) { + lineCopy.remove_prefix(cpLen); + continue; + } + + if (!currentFont.load(size, color, c)) { + c = U'?'; + if (!currentFont.load(size, color, c)) { + app_fatal("Missing fonts"); + } + } + const uint8_t frame = c & 0xFF; + + const ClxSprite glyph = currentFont.glyph(frame); + const int charWidth = glyph.width(); + + const auto byteIndex = static_cast(lineStartPos + currentPos); + + // Draw highlight + if (byteIndex >= opts.highlightRange.begin && byteIndex < opts.highlightRange.end) { + const bool lastInRange = static_cast(byteIndex + cpLen) == opts.highlightRange.end; + FillRect(out, characterPosition.x, characterPosition.y, + glyph.width() + (lastInRange ? 0 : curSpacing), glyph.height(), + opts.highlightColor); + } + + DrawFont(out, characterPosition, glyph, color, outline); + maybeDrawCursor(); + + // Move to the next position + characterPosition.x += charWidth + curSpacing; + currentPos += cpLen; + lineCopy.remove_prefix(cpLen); + } + assert(currentPos == text.size()); + maybeDrawCursor(); +} + uint32_t DoDrawString(const Surface &out, std::string_view text, Rectangle rect, Point &characterPosition, int lineWidth, int charactersInLine, int rightMargin, int bottomMargin, GameFontTables size, text_color color, bool outline, TextRenderOptions &opts) @@ -455,19 +537,26 @@ uint32_t DoDrawString(const Surface &out, std::string_view text, Rectangle rect, std::string_view remaining = text; size_t cpLen; - const auto maybeDrawCursor = [&]() { - if (opts.cursorPosition == static_cast(text.size() - remaining.size())) { - Point position = characterPosition; - MaybeWrap(position, 2, rightMargin, position.x, opts.lineHeight); - if (GetAnimationFrame(2, 500) != 0) { - FontStack baseFont = LoadFont(size, color, 0); - if (baseFont.has_value()) { - DrawFont(out, position, baseFont.glyph('|'), color, outline); - } - } - if (opts.renderedCursorPositionOut != nullptr) { - *opts.renderedCursorPositionOut = position; - } + // Track line boundaries + size_t lineStartPos = 0; + size_t lineEndPos = 0; + + const auto drawLine = [&]() { + std::string_view lineText = text.substr(lineStartPos, lineEndPos - lineStartPos); + if (!lineText.empty()) { + DrawLine( + out, + lineText, + characterPosition, + rect, + opts.flags, + curSpacing, + size, + color, + outline, + opts, + lineStartPos, + lineWidth); } }; @@ -487,8 +576,10 @@ uint32_t DoDrawString(const Surface &out, std::string_view text, Rectangle rect, const uint8_t frame = next & 0xFF; const uint16_t width = currentFont.glyph(frame).width(); if (next == U'\n' || characterPosition.x + width > rightMargin) { - if (next == '\n') - maybeDrawCursor(); + lineEndPos = text.size() - remaining.size(); + + drawLine(); + const int nextLineY = characterPosition.y + opts.lineHeight; if (nextLineY >= bottomMargin) break; @@ -506,26 +597,26 @@ uint32_t DoDrawString(const Surface &out, std::string_view text, Rectangle rect, } characterPosition.x = GetLineStartX(opts.flags, rect, lineWidth); + // Start a new line + lineStartPos = next == U'\n' ? (text.size() - remaining.size() + cpLen) : (text.size() - remaining.size()); + lineEndPos = lineStartPos; + if (next == U'\n') continue; } - const ClxSprite glyph = currentFont.glyph(frame); - const auto byteIndex = static_cast(text.size() - remaining.size()); - - // Draw highlight - if (byteIndex >= opts.highlightRange.begin && byteIndex < opts.highlightRange.end) { - const bool lastInRange = static_cast(byteIndex + cpLen) == opts.highlightRange.end; - FillRect(out, characterPosition.x, characterPosition.y, - glyph.width() + (lastInRange ? 0 : curSpacing), glyph.height(), - opts.highlightColor); - } + // Update end position as we add characters + lineEndPos = text.size() - remaining.size() + cpLen; - DrawFont(out, characterPosition, glyph, color, outline); - maybeDrawCursor(); + // Update position for the next character characterPosition.x += width + curSpacing; } - maybeDrawCursor(); + + // Draw any remaining characters in the last line + if (lineStartPos < lineEndPos) { + drawLine(); + } + return static_cast(remaining.data() - text.data()); } diff --git a/Source/engine/render/text_render.hpp b/Source/engine/render/text_render.hpp index 957d137d8..47ccf7f64 100644 --- a/Source/engine/render/text_render.hpp +++ b/Source/engine/render/text_render.hpp @@ -155,6 +155,8 @@ struct TextRenderOptions { /** @brief If a cursor is rendered, the surface coordinates are saved here. */ std::optional *renderedCursorPositionOut = nullptr; + + bool cursorStatic = false; }; /** diff --git a/test/fixtures/text_render_integration_test/cursor-end.png b/test/fixtures/text_render_integration_test/cursor-end.png new file mode 100644 index 0000000000000000000000000000000000000000..7de350d04b77a08d3195773e7a5398084666a244 GIT binary patch literal 1297 zcmW-geN0nV7{*^mk&E2oMK9VyDHpgxnGB(nFIPsZWwctPl4=o{RIIhcYGF8I=O~Q> zoLa`j`T?RPp*V~S*#HL^8Duhxh@Wc&88Rov&rMk%n@;5Jz?0wmyw5pr&R^%8gQ|7u z>;(Y}0D!HK%d$|np$fx1kQb{LB?5SWN~umm%bvOFnQJ|0)zj09G}?Q1V4__Iy+-J_ zLVr1R%MNZQkN_kOQ2+u8#4M1JpduiT29*R16zmp3xfrZEu*<=j4fVSLW3g}yBXEo) zFb;`R96~^mER2QWXu`sfVY9+<7LFswaRMg@j396lL1F|+kR%EOhl7q2BnJfq$>R__ z9>(L493IKzp-fUdf}(J0HgG74LsKL@>pYs~&@^&NK=TC11qeDyKt&;sLI`3IF^DKp zj6j53gosH(Bq2m%gcy+|K}Zl{nM@*=%alsFN+nmTRoU6c9THwPVQ9bmeGm*MX|<%w-MwqT1}ORqL`#S1V1)()<+(s%6Wi zT>b}azd%nfoEdk%yluPRXBq7H>{7GweBIWQhd()BQSUV=3bm=Z>qT-&L~?9UtYAq1 ziIG0QJZzh~di+V7WzO0&b2o9ht+|nKIBvg7n>r9oKQSW=jPs2C|pTNJrNP-LPL-8`Co9kdVl{+Kfh!)JIvb~_w<~coE#e) z8yp<$?Cfl8Y_!|$6%`dFB_(>je$%E+N~JO-B_%pKn$PF^`T3!T+-u)E+z!K?V)yrk zN0-3e*UY?~XlQU+tyTt&8DQob>L?)%JSJ8G%o|k5r0VR7zc$>i`7H)>Klz=R;^>H{ zLl>!E_MGi1oGR-6$gsVjV5{~5)n%`mRj|vo@Q?UfQ+|uP*2H`4qjT>ZJjCaewa z8^6ocbZsbn-Q|5gXxg{-gsD_|WqQVz$)04sXkL-Bq-Zj5ZRR<>^UDj1%;%QtKJ~1x(GsEWbg|_sL+oTk zK#<_)%1+C_jl}`iEwXxg@bve=0`$ATgskvuz?H;HGf3^;vaBIolj= zDEx2aZo-Z+|8!5^1@-JLMG~>{OxTEHRbIi1d*;OH$|Xy8>OUlw-fz_V}7 sp0DyR^3A(;ZKftuOD`lQAk9QtJVp<1nI5%7qdx|qNLwdsS*6|oKd4vVnE(I) literal 0 HcmV?d00001 diff --git a/test/fixtures/text_render_integration_test/cursor-middle.png b/test/fixtures/text_render_integration_test/cursor-middle.png new file mode 100644 index 0000000000000000000000000000000000000000..fed7cf064e147149c1ba08fe655e6e68811facc3 GIT binary patch literal 1308 zcmW-fdr(tX5XKh?a*R+i%a#ne)e<lQTk$zNcX4~hXFW(Qm%?dt!+MS^Tmcbo2?ba!`5@%la1-ns)tS^ zbmmfbEYMd2Bmjv+005tY2nI+Ne+F7hPBB`6A~-WnD~v1p2<-zt};Su~B5;?rC{Qa*wX;8Oue0}%WmL=YlC z5X2WC6(E9y2q8icfrvl|BN35^2#F+8DwW9PQl(O=QYkeWRdTW>H8nXsJyoyQS*>=p z+D_AR4h|ClEdVC~)BtSz{9ulzC+AO#0TR(V0PM3?`;2<-4L$h++S=gQF;K??n+`Oz&y&$Z8+&+BWy-F~v*qdbEuJ5#nJb$!xSfi%)DJovp} z{t^y}k?(;0NyE#ovS$s3ht}P9%pZ3gi0erU?by8Za+KHU4_KxU_e_5x(Fb4ei7j(= zeAfx=EiEt0%0_c?9_#gkNlCxRWF11G&ClCc>sj0fU+G4SoOs1@?EUi|XkdPpk%S9qlU|=AR$8&RYLm#=-l0Dc6gG~{A zb-Iye=(}~^{${eWvc_mM+R@k!_W6P;awxi=5^Dh*24oVkN@E(2AK%p$guNV|$XSzn zl-c~iwfxeq+l|uxt4r5isB&=%%&R(!-jvH{!ne&~DW$LdcV1AqKdygKaH_22n~AMs zo}Gf_S4`OtA7uPnVjGQLyYAf0EuMxoY$oq(&LUauk=peBv`B?y!__MD;_29${ChFe zBkDoJ3DpiCL#g?G+$LZDc1cj7a3o>Nm^^zZEaSviCzYOYpZr~FbB`MKUEJtmo+u91 z#vcx8SK7S0vM=}c{mhAfP7TkT`|`ghe=YE{)n{+I62sAiohi7HQrs2x*urr3zIQ3$ z7{N@G%w>pLzgt44MVDJ=b;$+V%wpbn%JJEg=DZNIz$0X1+_sXQAdd+5q|>`&`1wZV zp-JD4ofXBa@12?5e?4lsVb$wTb)|;5n56#Ee0_Cw73K1*OE|+(Oy{dEdJl^-0!Hw| zqJ{JySDo)GO|yfc!lteJ%6r=`>6S)Yf0Yh%LiYtL)CU$GeZKnpv6a8&x-$&5F4G-K z?&eiLjAwjt2zOifnc~j<fK-wrogC-C$aiR%}nrh6%8jFb5H8i4e zMKKZ?x0+BE?Py(5(SiX*fecz}sWPCL3bjtBXgWgGQIlTy=6AmH-FxQ#b?zQz)?DVO zuu%ZOl*^fal69h&OIEf%Jf+R>11%k~+&k2%^0)phS2`(4o zvPm|V6XX#6zo z8!_TFKm)*D01W`gKRoc!w0HQ{29Sf+0pS1S@PE|!KG5EO;P!3Uxf3)Cfx`jcO{c#^ z<6B9470@Dq(pb=h0WSz%y4{a2U2>m1*-%<~*~T;6XvH_x`?g?Xjc!ZrRc6yY`>m^qteN=BRf0{w%F|wjy67SsIt5oH#|o z;Z0?-!UKbFe{a>RUrU}g7qwqoUw_(k=GgKh`xfsiQg6(cuhgaHs)Vw%Xi>uF34+it z5+gqY|MSZOZRLMmE_&iv^Ppr|%a#T0UnRC=MqHd7e*88ZX2T$Tgo0b56RWr@9-We=}Vni6!+oX%Bak?r%Q_4Z|QFm zZ_oSY9XeF)`7`S5Rd0S-TNA(Rhl9q@`ph$3%$IvwV~?{A&(hB^WxV$!cdp$nRM|G$ z+HYlMWE?uNM`usz<3x>%632DFwl-V$9$Z+n**kvOfdl7l&Uw16Dvlv>+qC=2rM9XF z)AZ{X1}8HQ=Pyz%{hI6XB$szph1o(LWvqMnHgQE)aGktZvU^~A`uJHDcUqq*jS^PU zDZ=o!vLNi<68@@MKl>ejS_RH$(tDG#Yp4D+a_sLH1D$dSV~6|u2vJXWNr~*#iu%PA zpEaf5Xe=|U?h23dMjI=r;A?H2pF-!aE*0LN&^vuaSCX}HdT3Q!|Asj#>*!V6n)cr? zCv9+5m`QV(?Y`*SS9(WQn=WB(cTMi9u~FE^oxy{tap~%Ax5=lCjOiWG5&!@I literal 0 HcmV?d00001 diff --git a/test/fixtures/text_render_integration_test/highlight-full.png b/test/fixtures/text_render_integration_test/highlight-full.png new file mode 100644 index 0000000000000000000000000000000000000000..6d1e7f95d2d181df68b3e317bca4f23b297aedaf GIT binary patch literal 1292 zcmW-fdr%Ws6vmGwltmZ2v5RaV#6=be77>KjP=gQ&l8gyN2u(z+u>^$5@ED|u3I-}q zK^ln^)RL$uFg{vPE1gP%j{#(2D>Lbcm8v7uS}GMP_Jv`4;mq%R-*@ks``5YEs`xm! zg`Numz)dcbCL*sz?Ta}f%~vm31Kt-Yv5de06{Ue-fzHVhbG z;2?e11ol=S0Z1Hz0E9G%IUprLML;S8s%XfhVOKaD6oWwzCKCL9b2Hdi>#!EvNGPT&NA5d=;mNQ@u}l0<=^DD<2lDHISSpCb5t zj8BmipXBpVCTTuF(>OgBD4M1inq=mb&oC6jAf<&2Ux-wQV1k5n5YixoFa!~T2nr7o zh9eC}goqF#M7S6sMu?&j(Fn0r8ZDDa6$+V3B~zh-$p>`b$n)o56T znO(5p1wao#4S)uK$IcIC8D?_+)(DV-)&ame%iSJz-mOHY0MC4-?oKmSZh$_csrkVH}x5s?`h`axiz z)W;``$Mage7-P(`(_a)l4hxKn<)*0hQvG#gDVIhqr9`K`!qXiL3zu9m|(D2n73iybO2u1THti&CS=<6?b-i`SRt% zhYxKwTX%Q&>C>l8CR2HNc|k!zMn=Zw&6^bpMPy`TaB#3dAmDPj=p*-<_6%Q!;cl`0 ztIV-$V87MQ&P-Y?tp~I4SfjG~W4}ac)=BZd z?qA0nSH#}(xGnMybU&h3aFVKGTDB*q-ze!B#;RSDeG2`}ua8omkBkV`n{K8xR~S9tI{v)S2nEnDYYm^{73)V`Fz~F<3vo~yXiq=cxhy?tl`1Brm_{!*YNNh zd%Cx+V%yNVnss%uQU31lKOfKy*bBZ@=3Y=X(UV^O_O?xXj`W>Uup&o(b)L_coGZM9 zje7ODqU%Yem2c$6P-3Dj>xFEpdC@0N#(#VDqoZbNz=gej(W|!>@9=jd?JMnJy`OSz qQH5U=<@~**dD)G$!(MassCswbqhBV*kM(Uss|I;&y!7IFUD^LAgX3)g literal 0 HcmV?d00001 diff --git a/test/fixtures/text_render_integration_test/highlight-partial.png b/test/fixtures/text_render_integration_test/highlight-partial.png new file mode 100644 index 0000000000000000000000000000000000000000..f94036a78a9adb89787c9b75e97323ba8a65d7d7 GIT binary patch literal 1334 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 F{{u%VQ|bT! literal 0 HcmV?d00001 diff --git a/test/fixtures/text_render_integration_test/multiline_cursor-end_second_line.png b/test/fixtures/text_render_integration_test/multiline_cursor-end_second_line.png new file mode 100644 index 0000000000000000000000000000000000000000..c5fa0bf9742dbd8a3aa3349d095f61fe18ec6042 GIT binary patch literal 1513 zcmW-fdo+}J7{`B#_2Q+MUd%AY;B6aAa+zc@n5m(e8qCx%ZWRsEQf?)+L}gSmoJt~c z*(P-AP%bqo5kh0yQmnQuTXJOEt?ZJv+syv*oX_+9exKiSe*Zm3g+XhL4Xq3T0LFYC zcLVfH*a65wsIICS9{?5tp+M{p$IkigoVQLmb#`{adfHW|nr@8+T}fad7YyXl9+ZG# z1;7EoQQ!;!od#ICfQthm2BHWcWP=16*u?;OERY)uN_apK1}b*}L{}F@5DZ0d3?bnt zjfBx@xGtiLpm3v$KtaA0it3^$R20Qf3_~ys#UVI?;TVp?fRRY>JBE{BfZ!AoMxh`S z5>BGv6bj5Zje^l=DD7Jy(P$)sh7;eELJ%Z^fJ&ni6gpHogm9+QoS`~H=&le~h%>{L z&Vb5*xOzdnAPg3S1@U4-*bo+%%jWU80s&7bKOaa!0GSLl zw`#P763upES^)Yvpx6aStN>LXOsQ0(&CRN+s*d8~mfYNi)YS745tV%YF%D-hlbPV= z7V7B8wX^deldVlmP<{P@d#{_XjaHxUJ6V4Hu>8{AoSOKQ@-V49C@g!8AeqV8vfNX+ z#EC zZefKZxDn8fx6Ixxebuu6X?4nj!ma&3ZBWN}^sl$Qvc|f?nsVURg*ulU6)zh1q znHd`!Q>)cVrSjalb0sAuGMOwrJv}}?K0G{JAQ1TY__(;ZP^nZ?Q&V`zT_u^1Tft)` zYxr`)#7!{V+o1hCT~njT&CS)qwH9dS4LVG)RFl{$02V0tT#h*5z?(9oGzUAB%iRCA zrLLEUG+tfiW}9P>VfL=hFhuSlBU`ogRuFdTGB;`8@}lCEz1c5TPRu2=CWP$mOo=JL zKPD!gw#;+7Bl|henNm`o9?3V`nb%^UIpffhd^zw`#vu;>S+*ZuXBFKRV8}G{U-1lg z@a!3SdgO+!+1<3rdW+A#B{K&1k!^}dO9k7&a;;rd2 zJ`DLQN%VfxNAY^`4^@YZ1Wkz1amkQ{Y|OUNGwRU=$HD9J zb%S+_(xc5&c@4j;5AE@oYGq`t7#$ojBxmeNc5h7OJdCx@AK91uXK>!J6{Dr!g|^f+ zvl18IQj=`rY~&Y4KELkGIQMTv-`(Jt@0%o!V=FEiXX_|;tnofIVwk0Tr1L@8$2K2B;>Seh5Z1@8vM)Y$J(J;7u}-DPKY2*SmMNK3o0NLf7hUo}r}Wz)|Rc)mQ=e9=AdP)$U--7Rac*ga?217b8zi2wiq literal 0 HcmV?d00001 diff --git a/test/fixtures/text_render_integration_test/multiline_cursor-middle_second_line.png b/test/fixtures/text_render_integration_test/multiline_cursor-middle_second_line.png new file mode 100644 index 0000000000000000000000000000000000000000..c7f68105519801d96b35d11a40c0285a81f8e5bd GIT binary patch literal 1516 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 literal 0 HcmV?d00001 diff --git a/test/fixtures/text_render_integration_test/multiline_highlight.png b/test/fixtures/text_render_integration_test/multiline_highlight.png new file mode 100644 index 0000000000000000000000000000000000000000..6dab07894b50e032d6e11af6653b65f22849a346 GIT binary patch literal 1334 zcmW-fe>7BS7{?zQ>t-+R)Kz!JG~KwZu}NfD7MW?rGR!h-r)K;p7NKKG9d$_2Y|&4S ztY3wqMNv4Rq8}5j7CAbrmL?UoX=_#yW!uVT_RVuX@AvzC-sil3z0W~;ke|63#|!|> zrT&r-DNr;`F*M11QZB<#41<*BGh9AWK7w)J(+)@-5PTCrVc^_)1N&CPArXlk}@tquz-lS&VZL_0h@5}cja+S^N( zEOBA8tt~8YQ`7ESqfITk^6Jj~W0&^l)a}?^5x3=-DkdjLl`fVgd59ucxXG6;6R~;A z%~{qarnq6GYW#Ng(2b1VmhD$-)Mra#jvijOKSPn4B;6S06aKxxUufsyyvUiqfJ0*B z7hrhOJax0^d2`0Y@-2PY(Oo-3dVh533S8VMwl4LgvR2MdvM0i9aG!FJNv>U-hgze}ZE?(S`NcC|J(`8?h(HapJBDwxIcFgLfII~O-GdGqGY@bGYN zZ*OaBYh`6+K|w)QR#tLya$HyM+sHWGvXTnqe-bmq)=sz6|URd;e_u^pISA(s+9J- z`0{LyVt0$1@5o;#+Z|}x;;{f2L@yKEzay_a*vdr)3X||1rDl8f+hQ zrSkBh7T>AV!M766+R%W2fdIF%oPQg+dAE9Z;X@mwwFjzC*{BZjtCN>pI`cSV(TfXb zu86k(khuQmz?#n0O@R%g`ayyd=Ii>jWZv;yy-e^{;5_c48CTTHkFOadbW8g#6UQ49 z`-u{*+e#DjNzZ}xO~uXwDUP1S7o+tT7EDQ}8xq{blRkI89u-@J`i9R_>mNt@!1(Mf6Yc@hfVL241tZ-oFLJFgO0*AUHkPN91H)tNPsf*J}}q zE77&3ipmGqpSdq?s5N@Br`TTN5=`iitx9{GXJ^|H|6WeLE8|R_yncHBckw5LPQ`xX zJNIl}YcaMk*4(qJUenk7u*%L>DCdunu9!X5-ppO1kciD jnO1yX{HQLp`E7mDz*Q`enBH_9Ed@w@gCq^BqxSp{%|-xd literal 0 HcmV?d00001 diff --git a/test/text_render_integration_test.cpp b/test/text_render_integration_test.cpp index 004b0044c..6f4cacbe2 100644 --- a/test/text_render_integration_test.cpp +++ b/test/text_render_integration_test.cpp @@ -211,6 +211,116 @@ const TestFixture Fixtures[] { { "Two", UiFlags::ColorUiSilverDark }, }, }, + TestFixture { + .name = "cursor-start", + .width = 120, + .height = 15, + .fmt = "Hello World", + .opts = { + .flags = UiFlags::ColorUiGold, + .cursorPosition = 0, // Cursor at start + .cursorStatic = true, + }, + }, + TestFixture { + .name = "cursor-middle", + .width = 120, + .height = 15, + .fmt = "Hello World", + .opts = { + .flags = UiFlags::ColorUiGold, + .cursorPosition = 5, // Cursor after "Hello", + .cursorStatic = true, + }, + }, + TestFixture { + .name = "cursor-end", + .width = 120, + .height = 15, + .fmt = "Hello World", + .opts = { + .flags = UiFlags::ColorUiGold, + .cursorPosition = 11, // Cursor at end + .cursorStatic = true, + }, + }, + TestFixture { + .name = "multiline_cursor-end_first_line", + .width = 100, + .height = 50, + .fmt = "First line\nSecond line", + .opts = { + .flags = UiFlags::ColorUiGold, + .cursorPosition = 10, // Cursor at end of first line + .cursorStatic = true, + }, + }, + TestFixture { + .name = "multiline_cursor-start_second_line", + .width = 100, + .height = 50, + .fmt = "First line\nSecond line", + .opts = { + .flags = UiFlags::ColorUiGold, + .cursorPosition = 11, // Cursor at start of second line + .cursorStatic = true, + }, + }, + TestFixture { + .name = "multiline_cursor-middle_second_line", + .width = 100, + .height = 50, + .fmt = "First line\nSecond line", + .opts = { + .flags = UiFlags::ColorUiGold, + .cursorPosition = 14, // Cursor at second line, at the 'o' of "Second" + .cursorStatic = true, + }, + }, + TestFixture { + .name = "multiline_cursor-end_second_line", + .width = 100, + .height = 50, + .fmt = "First line\nSecond line", + .opts = { + .flags = UiFlags::ColorUiGold, + .cursorPosition = 22, // Cursor at start of second line + .cursorStatic = true, + }, + }, + TestFixture { + .name = "highlight-partial", + .width = 120, + .height = 15, + .fmt = "Hello World", + .opts = { + .flags = UiFlags::ColorUiGold, + .highlightRange = { 5, 10 }, // Highlight " Worl" + .highlightColor = PAL8_BLUE, + }, + }, + TestFixture { + .name = "highlight-full", + .width = 120, + .height = 15, + .fmt = "Hello World", + .opts = { + .flags = UiFlags::ColorUiGold, + .highlightRange = { 0, 11 }, // Highlight entire text + .highlightColor = PAL8_BLUE, + }, + }, + TestFixture { + .name = "multiline_highlight", + .width = 70, + .height = 50, + .fmt = "Hello\nWorld", + .opts = { + .flags = UiFlags::ColorUiGold, + .highlightRange = { 3, 8 }, // Highlight "lo\nWo" + .highlightColor = PAL8_BLUE, + }, + }, }; SDLPaletteUniquePtr LoadPalette()