From 307d9249ac8fec348aab2107809d36729da06fb4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=ADn=20Lucas=20Golini?= Date: Sat, 2 May 2026 01:12:13 -0300 Subject: [PATCH] Add `float` and `clear` support. Fixes in Base64 implementation. Fixes in remote image loading. --- .../html/eepp-ui-border-rendering-2.webp | Bin 3660 -> 3670 bytes .../assets/html/eepp-ui-border-rendering.webp | Bin 5118 -> 5154 bytes .../html/position_absolute_and_float.html | 176 +++++ include/eepp/graphics/linewrap.hpp | 7 +- include/eepp/graphics/richtext.hpp | 7 +- include/eepp/graphics/text.hpp | 6 +- include/eepp/graphics/textlayout.hpp | 2 +- include/eepp/system/base64.hpp | 18 +- include/eepp/ui/css/propertydefinition.hpp | 3 + include/eepp/ui/csslayouttypes.hpp | 16 + include/eepp/ui/uihtmlwidget.hpp | 8 + include/eepp/ui/uitextspan.hpp | 1 + src/eepp/graphics/csslayouttypes.cpp | 44 ++ src/eepp/graphics/drawablesearcher.cpp | 16 +- src/eepp/graphics/richtext.cpp | 469 ++++++++++++-- src/eepp/graphics/text.cpp | 2 +- src/eepp/graphics/textlayout.cpp | 6 +- src/eepp/system/base64.cpp | 25 +- src/eepp/ui/css/drawableimageparser.cpp | 15 +- src/eepp/ui/css/stylesheetspecification.cpp | 8 +- src/eepp/ui/uihtmlwidget.cpp | 29 +- src/eepp/ui/uirichtext.cpp | 9 +- src/eepp/ui/uitextspan.cpp | 8 +- src/tests/unit_tests/uihtml_float_tests.cpp | 607 ++++++++++++++++++ src/tests/unit_tests/utest.hpp | 18 +- 25 files changed, 1391 insertions(+), 109 deletions(-) create mode 100644 bin/unit_tests/assets/html/position_absolute_and_float.html create mode 100644 src/tests/unit_tests/uihtml_float_tests.cpp diff --git a/bin/unit_tests/assets/html/eepp-ui-border-rendering-2.webp b/bin/unit_tests/assets/html/eepp-ui-border-rendering-2.webp index 34d2d0c8adfc9251f9fa4040aa2f7519d5816ad6..9439bc1bb2daf9fe356dba3e8d5a4ecd72ff7026 100644 GIT binary patch delta 3391 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 C_RQe` diff --git a/bin/unit_tests/assets/html/eepp-ui-border-rendering.webp b/bin/unit_tests/assets/html/eepp-ui-border-rendering.webp index e28633cf4cd4c1a208f639d1ab88a93830020f86..19588b25acb028ac51a6cc0556d1e13d827233c6 100644 GIT binary patch delta 4857 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 delta 4808 zcma)Ai8qw*`^LnKV(fiv!&pOPPZ7qFv1KWQ$S!Mk)7V}kWyzlGjIA)q8X++##y%wb z_Q^Wd6507peLsJ|?_B3O=ee)zzOUz;=RNoHzRz1Ia(0FJu8x-0Zx#xQ`?uAN%#3d6 zud!4!n^OYLx+(ADt39XcJ*P@>Gu7{KSp(Ik6I<^%$;{}+c)jnkO`arw%Yi}O#`@%k zCIQ2+Og5&|5^9~4rTT8_fylk+0zp}57h7EC#!m-|6XXw@A2v1u$1#bT*{dRDVr8Zw zf?*O26P6vl!-r3Z3T;8|M$~-LPRT)=i+fcK)j*KpUF*8MtO4I{sokX#Nu>3!@xj5b z1A~T7Z1+G5zgs3%d&h`%hAOTzzA=MoZ}oLul=f>E<_^{xjvdSW)K}^aVcrjSpb(NNu2NUV19~t2AP?1bB0Dl5e5?NZ5;A=&8|2O{rp} z2>>r}HTg#rNh^~O*Z-k<>63p>V=oWVX!8s@6Phl&caodEA}FLKB$QEneK*$=`RbcQ z6qSlmt%Um;&ZyXNv5i#UDKWv6ysg4se*8U-_egC zXH{mK+mSz;pGNq&N4v4^F1mIpgYD}zD1oT0`i0C4EXsxFVrgjDF6THFWo7iu=h*en zM*anFgv7ptvHuH^4|o=)6B@Aql7#iI^}d<&%}nFUmeb|K?4LE1esMkN2C;~JdJTt^ zZY>g<>}ucr^?g|qK~(tkp(HkFezk3hA5ozv?J>zzl6;AEx8qvr$0MTIp6A^wBj=jZ*;%Xhi)U4qGIC#22c3lbq@F>SE z#I?#rOn&e*)7%J#I|XP2H8!V!BN7bP5ZAh=F;>lsnDK>l#Uvy@Dqk{j5mU90t(c_o zC-`5PvGW?fd_ai;=Ddc02TQ)|f(`OVFF`AUDAJS~ED;a4=G`dKjZfcFFOtd2N$E;~ z(_x$^|NO=M*KGpM^C=zAC6&`ai5ZymJ4Qev$M3`gZSXC7n%O1-6^fp?HJ$?;UfDgG z=+v5LlJ)L<(VEF8>bYYKstn%sfpKAwefW@AQc&FR@*%pgn)%6Rx;U2W&v9W2q|_qt zzPR6@IW5`j+I9Pb!lnNH^nrasO3>7_gAhAM8n)PwGdh0$oH!56QRn9kfSlkl)W}iVjRSKL(Xq6&(w0z=3FydPR zEJGZL5JfJn5x6fLpDUbk-r>8s-A8B!9VgT7TwRDpUVxRCstx4|kN1CjjMw+(&5Ry+ zqDgFvYM9T6o`AZejR5OSd)vdZ?EkJPZ{wILeRNx$(J%{y|?t)aPm zDhrvNaOuN1B?>Cg(8pQJx!Vq@KAnI4G8|p|L)o(42sQYD%+;mh7pc_v$sTy zv#WdnrhZQIKeTyzu=JhQ7tpJ=d$7&IWaY#6IEMI!cLue8&6f{LGiwX&DC=4}0=oN@ z2yxE~DCNG!HI+2E*fws5x1ATXolj$if$dU9I+-W}JE0$DcK2H!ALvyK&su@TwuW<8*7lff+Ap&g4;U`gIh}Wyp_kgf z(wrQke!rq&?R;`~=ACO6VrE~TT=ufCH|0o3vo|cEIvpgmZ4n6SJEf(J@+iuN1^r0F z*>Jg1?JcbokFS@PUDiJ37eBt)Dan#R6twAl9NwtPm!BVW27Q37%x)=@11N}M_sdux zg?@FeI#wKQQ!kuRwmi@c4(*3wHNh$+2FtVc#kDuXI}d zxEYGxv&gv4m^!xIw`N(wKT26H`5^g56kW?=$_WLR;}~Bm5yd-4o$&5g0{3A+Rn}~{!4|&IaPH?_{ye?`9l4Nv zn7uF9j3HjD$Zim3`Ey=1;+(2)czqMQVU?ePd-I`%7Kj->y#p1}`?$T@aAks_>WK4I z86I-`xk^|GrR_8ONd6A4n6U7hikHnWBOI}GI%02lZ7ULv{rHYSx zU|eSb@SUh9|Jxm`vZj7Y;TIrYz9XfctVUaM7#_zH(X$Kc8>+6l=$|Uf%8`0KZW+`R z3=vb=&Ntm()rXBQ!=$npR@eAS>9o4qZ-m%XP;A)A@e*e=h$hRhprFKGzSOo1Mae+U z?{Kq>Gted8CUVMn@{zy8k|+Pl(nWlwNE|>;qDX$TrYyY~mwuI^Rs0I9JZUx|on7)i zMUb?CqWMoWn7xJRn8GEhb#^kap`_E0_3x8XIP)wI!KfsI`134wsO_A~J`y-km0NAyLVV3cr5d!9 z`G?(skD@-aE|C_wUY^OTW!%N>`0w<0pJl=IMV-)!hcwTCJ3sasI?8&l9{;@4N&lN? zG57e3&;Cnq^#R$-bimw(p+ZI^i*H`FZPgd^W$alYhXDcI8F6Tb{g4zrQg)OFVg5_d zdW^SGGe-GbXbXL;dJ+C_WX>b;3}W@gW%Q-)&mtT;>_|rTmET1tDw?TEW^K2;_%hjr z1ZWo^5lhoy`pMJP7dz3HNEl<86sZJw!@y=&Y!>SC69&LyMM99P$WhRaTg@9gmB<$P zlSgLofwr(#H)9^E&#I-$SCy{as`sDE=fJjzcRn!+AB+nqe|h6RX6Vj$D{3u;7(X`l znUDcf+tfegPt|Ssf#iW#$Y+|FQDqOG<)($mLubxhDSPai_*itgl96q;+is#Cggo>1 zdIJN5YPJwQp5AxX1;pe~`KC0ZFcDvU#iroT&9AbQi2Sbuz!zn4uX2G*J;9o=zKfLOjwzD9>qV-7c_=r}DEU?a#+-no)&);=pU47G2BC@Pr z2~GQya~q*#G1ryZht2|h-fJsKsnIY!Lhn4h+?~oVN+Gp-M^2rT z(EU+<;4W32y&P<$se?+rm@7Q(jFO}Of^NCoVh(85w)$4QQIo@tyt^o-W*q9({0Nw^ zqvV^UN7Mp{z+dmO)@t11^Kq|s<) z@*+;bhd0-m=xfjOwe4>nY-j1)P7hWJ)|W8hEQyttXg%7I=VSgx>(EP)Qk;~ldHM)v zMsvr2S-ZBtCq>rru*>Xra#TsuKqfp>Eh#jugG)zBt*Im8b^PAE>LVFUOP$^*{P8U2 zZ`20$5_#XqMM=o~<3f@BktsZ<`vNX=3gJ8JgXK-=W*Q%xxcgRv*CJ=0h`g+EeP`%P zQhbQNw4(s8HQfaHbY&#>tU?&*Ml=Vzg|m4NhdPvy?dsoNcmepPE6}(Fsr3g@NQ3&r zF~2=9GasO#i=E$DIA4`tH)2+nDwFVRy?=!wn!|;b-KGCB`xV}kuf;4@W@%)Z^N*~- z$uSuYceXY0#7R7Wi#|(BB`bQn_=?oihp*ve!MM8}ROfifAO4npH&!%t)cFAUq}Wg< z)LfAfV3e&Yb^I4&SwpRu^o=GcVbusoxSw3Ftbw>{&<+%__Bx0{5gRg3cKy{Ef z--@Vp_>8P~>;J-)2$u6~7cfO%%TlHrEq3Nj?_3f*uyZ`2zR1Zk$a^vD!S7BFb1(X_ z<2V(ryuj(J%UZqKgG@HoPE(R-$5~`DeFLjFVDU8ay^DJZjCAw6%1UUnDRs?zj>7!z zF8N{hM)?HKKzxr<0N>SrzS}`HOp0cY>&)P_{ftik(`5r^_lzKRGg0G5OD-Q7{#`0o z-d7w=*yZ*~yi_q+%FISS;$J4Z7fbsZ*U|Fj95d>S!lyzZl9kw#w)DW_mFQ3Fz=r>0 z_uG0Z6+-dJ0_$9C>g@Y`+HSVJp^$&k;54j}u9^cUp{tH;;m&<*0s14m{E?$Q+NPU9 zv!Rgq(PPF}a@)j1Q9V}X@u*V40YeL_ykGayR*PQ|$Rx3rXPM&q5IU+2@Lq{#q(dh_ z+LFiX84OH-C&N7&e=3s@8?V+H z9W-mCSyh$thk`%X#XKG^jNOG*O(qYDJ-@&(Xe>O8!n0O4q z$?xzKUiX)n8@gp%zZ9-ZW`qjeS#IJw7mbibd6mm;r#O(yNJhvTS#?qrS`s{>t2rtx z6F|7`Sim3b-MU7>NwVe_|4Cl`RdjU!k04>sKUyl4~9sz9O}Md+e2J+YPY zyj3{SE&Ln}ZbOsK0Hm5j1+U4Q?I%p5G_%vxVS3lhO|UJX0hOj|IeK36Vm{!4L?g(~ zqcHn@sY)n#Bhw)M2j=zzh}tLr6uCSUtGMzlWTxR9rKEr=!#g3|rz;o-f%Cycj@7Ry z{1$11j#pa)V(ANR&%j;&RsWyuciBXMWPU+$1y+IcHhJ46KX}`K zHs`WXr*m;y`UDdjKrzxL7vw!=*?(px=2d*Srnb0&sds-T444L3-TS!s5ag@d-VtGk$gOLdMyH`hO!R5^!lIQ54QLvyb$qwVqw+ILOV3;J@1Aa;0admXpiNey*SrP z)RGhtGIfja*=r5Y_R1}-A+_0a=U%YgX;cA3P79y5H&#_@0hwH5iuK+Pe=O!2p!d=& zttZnyQ6Jp~KYMywZ@hfv!Cvcj7>hQc=jn74hf}6anYOaetdh|7bjzb7pHbbOTh@DY zbq}i|D{c`!1SUHj>fRQ^<-IL9*blyS!<-YZIGaGK$L=sW$_Ct0~3=DvHPOn z73`=C2dg2$9(c;C^$)d`*!6>&+tHu;_CKthL##}e6#R|xI<*qM+DyM?6uc6%r6~Mn zF@*OZngLV$9ku`c9C(t4X8M$nm^Uqilz$~p@wNEc&0-5lk;co9%wzAp(8@mtx{AaY z8n7r$W`5$?5{(>AOTfhTF7)3Lua6f19aEzv2-qCFU?r%l>#c)o2;UOq8;C3 zMZHg&1zuE0D&!l|eR8EEe+>2(X!GfC2oD4JYQnFC+vmR$Xvib#9CCJv46c&i)_GNt Li1YssZmj(u3v@>Q diff --git a/bin/unit_tests/assets/html/position_absolute_and_float.html b/bin/unit_tests/assets/html/position_absolute_and_float.html new file mode 100644 index 000000000..23d1e4cd6 --- /dev/null +++ b/bin/unit_tests/assets/html/position_absolute_and_float.html @@ -0,0 +1,176 @@ + + + + ...::: Welcome to the Matrix :::... + + + + + +
+
+
File Upload
+ +
+
+
Download Files
+
+ ENTER +
+
+
+
+ + diff --git a/include/eepp/graphics/linewrap.hpp b/include/eepp/graphics/linewrap.hpp index 334b54ecf..3bff7913b 100644 --- a/include/eepp/graphics/linewrap.hpp +++ b/include/eepp/graphics/linewrap.hpp @@ -2,7 +2,6 @@ #include #include -#include namespace EE::Graphics { @@ -11,12 +10,12 @@ enum class LineWrapMode { NoWrap, Letter, Word }; enum class LineWrapType { Viewport, LineBreakingColumn }; struct LineWrapInfo { - std::vector wraps; // Each wrap character position (where the wrap must happen) - Float paddingStart{ 0 }; // Padding of the wrapped lines + SmallVector wraps; // Each wrap character position (where the wrap must happen) + Float paddingStart{ 0 }; // Padding of the wrapped lines }; struct LineWrapInfoEx : public LineWrapInfo { - std::vector wrapsWidth; // Each wrap width + SmallVector wrapsWidth; // Each wrap width }; class EE_API LineWrap { diff --git a/include/eepp/graphics/richtext.hpp b/include/eepp/graphics/richtext.hpp index 1c7a90ced..e746b8fd5 100644 --- a/include/eepp/graphics/richtext.hpp +++ b/include/eepp/graphics/richtext.hpp @@ -3,6 +3,7 @@ #include #include +#include #include #include #include @@ -83,6 +84,8 @@ class EE_API RichText : public Drawable { struct CustomBlock { Sizef size; bool isBlock{ false }; + UI::CSSFloat floatType{ UI::CSSFloat::None }; + UI::CSSClear clearType{ UI::CSSClear::None }; }; struct SpanBlock { @@ -104,7 +107,9 @@ class EE_API RichText : public Drawable { * @param size The physical dimensions of the spacer. * @param isBlock Whether this spacer acts as a block-level element. */ - void addCustomSize( const Sizef& size, bool isBlock = false ); + void addCustomSize( const Sizef& size, bool isBlock = false, + UI::CSSFloat floatType = UI::CSSFloat::None, + UI::CSSClear clearType = UI::CSSClear::None ); /** @return The list of blocks. */ const std::vector& getBlocks() { return mBlocks; } diff --git a/include/eepp/graphics/text.hpp b/include/eepp/graphics/text.hpp index b832f861d..461f6f301 100644 --- a/include/eepp/graphics/text.hpp +++ b/include/eepp/graphics/text.hpp @@ -258,7 +258,7 @@ class EE_API Text { void setShadowColor( const Color& color ); /** @return Every cached text line width */ - const std::vector& getLinesWidth(); + const SmallVector& getLinesWidth(); /** @return The last line width */ Float getLastLineWidth(); @@ -413,8 +413,8 @@ class EE_API Text { TextDirection mDirection{ TextDirection::Unspecified }; Vector2f mInitialOffset{ 0.f, 0.f }; - mutable std::vector mVisualLines; - mutable std::vector mLinesWidth; + mutable SmallVector mVisualLines; + mutable SmallVector mLinesWidth; std::vector mVertices; std::vector mColors; diff --git a/include/eepp/graphics/textlayout.hpp b/include/eepp/graphics/textlayout.hpp index a52986342..d8802d3e4 100644 --- a/include/eepp/graphics/textlayout.hpp +++ b/include/eepp/graphics/textlayout.hpp @@ -30,7 +30,7 @@ class EE_API TextLayout { bool isRTL() const { return direction == TextDirection::RightToLeft; } - std::vector getLinesWidth() const; + SmallVector getLinesWidth() const; static Cache layout( const String& string, Font* font, const Uint32& fontSize, const Uint32& style, const Uint32& tabWidth = 4, diff --git a/include/eepp/system/base64.hpp b/include/eepp/system/base64.hpp index 8ee14c4b6..640dcf9ef 100644 --- a/include/eepp/system/base64.hpp +++ b/include/eepp/system/base64.hpp @@ -1,11 +1,11 @@ #ifndef EE_SYSTEM_BASE64_HPP #define EE_SYSTEM_BASE64_HPP -#include #include #include #include #include +#include namespace EE { namespace System { @@ -13,25 +13,29 @@ class EE_API Base64 { public: /** Encode binary data into base64 digits with MIME style === pads ** @return The final length of the output */ - static int encode( size_t in_len, const unsigned char* in, size_t out_len, char* out ); + static size_t encode( size_t in_len, const unsigned char* in, size_t out_len, char* out ); /** Decode base64 digits with MIME style === pads into binary data ** @return The final length of the output */ - static int decode( size_t in_len, const char* in, size_t out_len, unsigned char* out ); + static size_t decode( size_t in_len, const char* in, size_t out_len, unsigned char* out ); /** Encodes a string into a base64 string ** @return True if encoding was successful */ - static bool encode( const std::string& in, std::string& out ); + static bool encode( std::string_view in, std::string& out ); /** Decodes a base64 string to a string ** @return True if encoding was successful */ - static bool decode( const std::string& in, std::string& out ); + static size_t decode( std::string_view in, std::string& out ); /** @return A safe encoding output length for an input of the length indicated */ - static inline int encodeSafeOutLen( size_t in_len ) { return in_len / 3 * 4 + 4 + 1; } + static inline size_t encodeSafeOutLen( size_t in_len ) { + return ( ( in_len + 2 ) / 3 ) * 4 + 1; + } /** @return A safe decoding output length for an input of the length indicated */ - static inline int decodeSafeOutLen( size_t in_len ) { return in_len / 4 * 3 + 1; } + static inline size_t decodeSafeOutLen( size_t in_len ) { + return ( ( in_len + 3 ) / 4 ) * 3 + 1; + } }; }} // namespace EE::System diff --git a/include/eepp/ui/css/propertydefinition.hpp b/include/eepp/ui/css/propertydefinition.hpp index 49b65b4b3..9352fe0b4 100644 --- a/include/eepp/ui/css/propertydefinition.hpp +++ b/include/eepp/ui/css/propertydefinition.hpp @@ -250,11 +250,14 @@ enum class PropertyId : Uint32 { ListStyleType = String::hash( "list-style-type" ), ListStylePosition = String::hash( "list-style-position" ), ListStyleImage = String::hash( "list-style-image" ), + Float = String::hash( "float" ), + Clear = String::hash( "clear" ), DataLanguage = String::hash( "data-language" ), // Minor hack Action = String::hash( "action" ), Method = String::hash( "method" ), Enctype = String::hash( "enctype" ), Overflow = String::hash( "overflow" ), + Target = String::hash( "target" ), }; enum class PropertyType : Uint32 { diff --git a/include/eepp/ui/csslayouttypes.hpp b/include/eepp/ui/csslayouttypes.hpp index e301aa4b7..da9b10a74 100644 --- a/include/eepp/ui/csslayouttypes.hpp +++ b/include/eepp/ui/csslayouttypes.hpp @@ -61,6 +61,22 @@ struct CSSListStylePositionHelper { static CSSListStylePosition fromString( std::string_view val ); }; +enum class CSSFloat { None, Left, Right }; + +struct CSSFloatHelper { + static std::string toString( CSSFloat val ); + + static CSSFloat fromString( std::string_view val ); +}; + +enum class CSSClear { None, Left, Right, Both }; + +struct CSSClearHelper { + static std::string toString( CSSClear val ); + + static CSSClear fromString( std::string_view val ); +}; + }} // namespace EE::UI #endif diff --git a/include/eepp/ui/uihtmlwidget.hpp b/include/eepp/ui/uihtmlwidget.hpp index 865d58fde..b2fcd64e3 100644 --- a/include/eepp/ui/uihtmlwidget.hpp +++ b/include/eepp/ui/uihtmlwidget.hpp @@ -36,6 +36,12 @@ class EE_API UIHTMLWidget : public UILayout { CSSPosition getCSSPosition() const { return mPosition; } void setCSSPosition( CSSPosition position ); + CSSFloat getCSSFloat() const { return mFloat; } + void setCSSFloat( CSSFloat cssFloat ); + + CSSClear getCSSClear() const { return mClear; } + void setCSSClear( CSSClear cssClear ); + const Rectf& getOffsets() const { return mOffsets; } void setOffsets( const Rectf& offsets ); @@ -68,6 +74,8 @@ class EE_API UIHTMLWidget : public UILayout { protected: CSSDisplay mDisplay{ CSSDisplay::Block }; CSSPosition mPosition{ CSSPosition::Static }; + CSSFloat mFloat{ CSSFloat::None }; + CSSClear mClear{ CSSClear::None }; std::string mTopEq{ "auto" }; std::string mRightEq{ "auto" }; std::string mBottomEq{ "auto" }; diff --git a/include/eepp/ui/uitextspan.hpp b/include/eepp/ui/uitextspan.hpp index c41cf0281..39a856fe3 100644 --- a/include/eepp/ui/uitextspan.hpp +++ b/include/eepp/ui/uitextspan.hpp @@ -173,6 +173,7 @@ class EE_API UIAnchorSpan : public UITextSpan { UIAnchorSpan( const std::string& tag = "a" ); std::string mHref; + std::string mTarget; virtual Uint32 onKeyDown( const KeyEvent& event ); diff --git a/src/eepp/graphics/csslayouttypes.cpp b/src/eepp/graphics/csslayouttypes.cpp index 38b5acd4a..3a3e92253 100644 --- a/src/eepp/graphics/csslayouttypes.cpp +++ b/src/eepp/graphics/csslayouttypes.cpp @@ -143,4 +143,48 @@ CSSListStylePosition CSSListStylePositionHelper::fromString( std::string_view va return CSSListStylePosition::Outside; } +std::string CSSFloatHelper::toString( CSSFloat val ) { + switch ( val ) { + case CSSFloat::Left: + return "left"; + case CSSFloat::Right: + return "right"; + case CSSFloat::None: + default: + return "none"; + } +} + +CSSFloat CSSFloatHelper::fromString( std::string_view val ) { + if ( val == "left" ) + return CSSFloat::Left; + if ( val == "right" ) + return CSSFloat::Right; + return CSSFloat::None; +} + +std::string CSSClearHelper::toString( CSSClear val ) { + switch ( val ) { + case CSSClear::Left: + return "left"; + case CSSClear::Right: + return "right"; + case CSSClear::Both: + return "both"; + case CSSClear::None: + default: + return "none"; + } +} + +CSSClear CSSClearHelper::fromString( std::string_view val ) { + if ( val == "left" ) + return CSSClear::Left; + if ( val == "right" ) + return CSSClear::Right; + if ( val == "both" ) + return CSSClear::Both; + return CSSClear::None; +} + }} // namespace EE::UI diff --git a/src/eepp/graphics/drawablesearcher.cpp b/src/eepp/graphics/drawablesearcher.cpp index 78f80a80a..0485ea5c0 100644 --- a/src/eepp/graphics/drawablesearcher.cpp +++ b/src/eepp/graphics/drawablesearcher.cpp @@ -78,16 +78,14 @@ static Drawable* parseDataURI( const std::string& name ) { format.svgScale( PixelDensity::getPixelDensity() ); if ( decodingType == "base64" ) { int fileStart = formatAndEncSep + 1; - int base64Size = name.size() - fileStart; - int bufSize = Base64::decodeSafeOutLen( base64Size ); - if ( bufSize <= 0 ) - return nullptr; - ScopedBuffer buffer( bufSize ); - int len = Base64::decode( base64Size, &name[fileStart], bufSize, buffer.get() ); - if ( len > 0 ) + std::string_view fileBase64 = std::string_view{ name }.substr( fileStart ); + std::string buffer; + int len = Base64::decode( fileBase64, buffer ); + if ( len > 0 ) { tex = TextureFactory::instance()->loadFromMemory( - buffer.get(), len, false, Texture::ClampMode::ClampToEdge, false, false, - format ); + (const unsigned char*)buffer.c_str(), buffer.size(), false, + Texture::ClampMode::ClampToEdge, false, false, format ); + } } else if ( decodingType == "urldecode" ) { int fileStart = formatAndEncSep + 1; std::string decoded( URI::decode( name.substr( fileStart ) ) ); diff --git a/src/eepp/graphics/richtext.cpp b/src/eepp/graphics/richtext.cpp index 6030aa17c..e0c458ffe 100644 --- a/src/eepp/graphics/richtext.cpp +++ b/src/eepp/graphics/richtext.cpp @@ -306,8 +306,9 @@ void RichText::addDrawable( std::shared_ptr drawable ) { invalidateLayout(); } -void RichText::addCustomSize( const Sizef& size, bool isBlock ) { - mBlocks.push_back( CustomBlock{ size, isBlock } ); +void RichText::addCustomSize( const Sizef& size, bool isBlock, UI::CSSFloat floatType, + UI::CSSClear clearType ) { + mBlocks.push_back( CustomBlock{ size, isBlock, floatType, clearType } ); invalidateLayout(); } @@ -447,6 +448,241 @@ void RichText::updateLayout() { if ( !mNeedsLayoutUpdate ) return; + // Detect whether any block has float/clear — if not, use the original + // non-float layout path which is simpler and faster. + bool hasFloats = false; + for ( auto& block : mBlocks ) { + if ( auto pSize = std::get_if( &block ) ) { + if ( pSize->floatType != UI::CSSFloat::None || + pSize->clearType != UI::CSSClear::None ) { + hasFloats = true; + break; + } + } + } + + // ─── Fast path: no floats or clears ───────────────────────────── + if ( !hasFloats ) { + mLines.clear(); + mLines.push_back( RenderParagraph() ); + + Float curX = 0; + Float maxWidth = 0; + Int64 curCharIdx = 0; + + // Pass 1: flow blocks into lines, wrapping at mMaxWidth. + for ( auto& block : mBlocks ) { + if ( auto pText = std::get_if( &block ) ) { + auto& span = pText->text; + if ( !span ) + continue; + + // Empty-string spans contribute only their margin/padding. + if ( span->getString().empty() ) { + Float l = pText->margin.Left + pText->padding.Left; + Float r = pText->margin.Right + pText->padding.Right; + if ( l <= 0 && r <= 0 ) + continue; + curX += l + r; + if ( !mLines.empty() ) + mLines.back().width += l + r; + continue; + } + + auto& fontStyle = span->getFontStyleConfig(); + if ( !fontStyle.Font ) + continue; + + Float extraLeft = pText->margin.Left + pText->padding.Left; + curX += extraLeft; + if ( !mLines.empty() ) + mLines.back().width += extraLeft; + + Uint32 textHints = span->getTextHints(); + + // Compute where lines break within this text span. + LineWrapInfoEx wrapInfo = LineWrap::computeLineBreaksEx( + span->getString(), fontStyle, mMaxWidth > 0 ? mMaxWidth : 1e9f, + mMaxWidth > 0 ? LineWrapMode::Word : LineWrapMode::NoWrap, false, 4, 0.f, + textHints, false, curX ); + + if ( wrapInfo.wraps.empty() || + wrapInfo.wraps.back() != (Float)span->getString().size() ) + wrapInfo.wraps.push_back( span->getString().size() ); + + // Emit a RenderSpan for each segment, wrapping to new lines as needed. + for ( size_t i = 0; i < wrapInfo.wraps.size() - 1; ++i ) { + size_t startIdx = wrapInfo.wraps[i]; + size_t endIdx = wrapInfo.wraps[i + 1]; + bool isNewline = + ( endIdx - startIdx == 1 && span->getString()[startIdx] == '\n' ); + + if ( !isNewline ) { + std::shared_ptr renderSpanText = std::make_shared(); + renderSpanText->setString( + span->getString().substr( startIdx, endIdx - startIdx ) ); + renderSpanText->setStyleConfig( fontStyle ); + + Float ascent = fontStyle.Font->getAscent( fontStyle.CharacterSize ); + Float height = fontStyle.Font->getLineSpacing( fontStyle.CharacterSize ); + Float spanWidth = renderSpanText->getTextWidth(); + + RenderSpan renderSpan; + renderSpan.block = + SpanBlock{ renderSpanText, pText->margin, pText->padding }; + renderSpan.position = { curX, 0 }; + renderSpan.size = Sizef( spanWidth, height ); + renderSpan.startCharIndex = curCharIdx; + renderSpan.endCharIndex = curCharIdx + ( endIdx - startIdx ); + curCharIdx = renderSpan.endCharIndex; + + RenderParagraph& currentLine = mLines.back(); + currentLine.spans.push_back( renderSpan ); + + currentLine.maxAscent = std::max( currentLine.maxAscent, ascent ); + currentLine.height = std::max( currentLine.height, height ); + + curX += spanWidth; + currentLine.width += spanWidth; + } + + // After the last segment, add trailing margin and check if the + // margin itself forces a wrap. + if ( i == wrapInfo.wraps.size() - 2 && !isNewline ) { + Float extraRight = pText->margin.Right + pText->padding.Right; + curX += extraRight; + mLines.back().width += extraRight; + if ( !isNewline && mMaxWidth > 0 && curX > mMaxWidth ) { + maxWidth = std::max( maxWidth, curX ); + mLines.push_back( RenderParagraph() ); + curX = 0; + continue; + } + } + + // Start a new line for hard breaks (newlines) or soft wraps. + if ( i < wrapInfo.wraps.size() - 2 || isNewline ) { + if ( isNewline ) { + curCharIdx++; + if ( i == wrapInfo.wraps.size() - 2 ) { + Float extraRight = pText->margin.Right + pText->padding.Right; + curX += extraRight; + mLines.back().width += extraRight; + } + } + maxWidth = std::max( maxWidth, curX ); + mLines.push_back( RenderParagraph() ); + curX = 0; + } + } + } else { + // Drawable or CustomBlock (non-float). + Sizef blockSize; + bool isBlock = false; + if ( auto pDrawable = std::get_if>( &block ) ) { + auto& drawable = *pDrawable; + blockSize = drawable ? drawable->getPixelsSize() : Sizef(); + } else if ( auto pSize = std::get_if( &block ) ) { + blockSize = pSize->size; + isBlock = pSize->isBlock; + } + + // Block elements force a line break before themselves. + if ( isBlock && curX > 0 ) { + maxWidth = std::max( maxWidth, curX ); + mLines.push_back( RenderParagraph() ); + curX = 0; + } + + // Inline elements that don't fit wrap to the next line. + if ( mMaxWidth > 0 && !isBlock && + ( curX + blockSize.getWidth() >= mMaxWidth || curX >= mMaxWidth ) && + curX > 0 ) { + maxWidth = std::max( maxWidth, curX ); + mLines.push_back( RenderParagraph() ); + curX = 0; + } + + RenderSpan renderSpan; + renderSpan.block = block; + renderSpan.position = { curX, 0 }; + renderSpan.size = blockSize; + renderSpan.startCharIndex = curCharIdx; + renderSpan.endCharIndex = curCharIdx + 1; + curCharIdx = renderSpan.endCharIndex; + + RenderParagraph& currentLine = mLines.back(); + currentLine.spans.push_back( renderSpan ); + + currentLine.maxAscent = std::max( currentLine.maxAscent, blockSize.getHeight() ); + currentLine.height = std::max( currentLine.height, blockSize.getHeight() ); + + curX += blockSize.getWidth(); + currentLine.width += blockSize.getWidth(); + + // Block elements also force a line break after themselves. + if ( ( mMaxWidth > 0 && curX >= mMaxWidth ) || isBlock ) { + maxWidth = std::max( maxWidth, curX ); + mLines.push_back( RenderParagraph() ); + curX = 0; + } + } + } + + maxWidth = std::max( maxWidth, curX ); + + // Remove trailing empty line if present. + if ( !mLines.empty() && mLines.back().spans.empty() && mLines.size() > 1 ) { + mLines.pop_back(); + } + + // Pass 2: assign Y positions to each line, apply text alignment, + // and compute vertical offsets for spans within their line. + Float curY = 0; + for ( auto& line : mLines ) { + line.y = curY; + + // Compute horizontal alignment offset for this line. + Float xOffset = 0; + if ( mMaxWidth > 0 && mAlign != 0 ) { + Uint32 hAlign = Font::getHorizontalAlign( mAlign ); + if ( hAlign == TEXT_ALIGN_CENTER ) { + xOffset = ( mMaxWidth - line.width ) * 0.5f; + } else if ( hAlign == TEXT_ALIGN_RIGHT ) { + xOffset = mMaxWidth - line.width; + } + } + + Float maxLineHeight = 0; + for ( auto& span : line.spans ) { + if ( auto pText = std::get_if( &span.block ) ) { + auto& textBlock = pText->text; + Float offsetY = line.maxAscent - textBlock->getCharacterSize(); + span.position.x += xOffset; + span.position.y = offsetY; + maxLineHeight = std::max( maxLineHeight, offsetY + span.size.getHeight() ); + } else { + Float offsetY = line.maxAscent - span.size.getHeight(); + if ( offsetY < 0 ) + offsetY = 0; + span.position.x += xOffset; + span.position.y = offsetY; + maxLineHeight = std::max( maxLineHeight, offsetY + span.size.getHeight() ); + } + } + + line.height = std::max( line.height, maxLineHeight ); + curY += line.height; + } + + mSize = Sizef( maxWidth, curY ); + mTotalCharacterCount = curCharIdx; + mNeedsLayoutUpdate = false; + return; + } + + // ─── Float-aware path ──────────────────────────────────────────── + mLines.clear(); mLines.push_back( RenderParagraph() ); @@ -454,8 +690,64 @@ void RichText::updateLayout() { Float maxWidth = 0; Int64 curCharIdx = 0; + // Active float rectangles: { left, top, right, bottom } in local coords. + std::vector leftFloats; + std::vector rightFloats; + Float curY = 0; + + // ── Helper lambdas ───────────────────────────────────────────── + // Returns the rightmost x-coordinate occupied by left floats at the given y. + auto floatLeftEdge = [&]( Float y ) -> Float { + Float l = 0; + for ( auto& f : leftFloats ) { + if ( y >= f.Top && y < f.Bottom ) + l = std::max( l, f.Right ); + } + return l; + }; + + // Returns the leftmost x-coordinate occupied by right floats at the given y. + auto floatRightEdge = [&]( Float y ) -> Float { + Float r = mMaxWidth > 0 ? mMaxWidth : 1e9f; + for ( auto& f : rightFloats ) { + if ( y >= f.Top && y < f.Bottom ) + r = std::min( r, f.Left ); + } + return r; + }; + + // Available horizontal space at y, narrowed by active floats on both sides. + auto effectiveMaxWidthAt = [&]( Float y ) -> Float { + return floatRightEdge( y ) - floatLeftEdge( y ); + }; + + // Advances curY past the bottom of active floats specified by clearType. + // Returns true if curY was moved. + auto clearFloats = [&]( UI::CSSClear clearType ) -> bool { + bool advanced = false; + if ( clearType == UI::CSSClear::Left || clearType == UI::CSSClear::Both ) { + for ( auto& f : leftFloats ) { + if ( f.Bottom > curY ) { + curY = f.Bottom; + advanced = true; + } + } + } + if ( clearType == UI::CSSClear::Right || clearType == UI::CSSClear::Both ) { + for ( auto& f : rightFloats ) { + if ( f.Bottom > curY ) { + curY = f.Bottom; + advanced = true; + } + } + } + return advanced; + }; + + // ── Pass 1: flow blocks with float awareness ──────────────────── for ( auto& block : mBlocks ) { if ( auto pText = std::get_if( &block ) ) { + // ── Text span ───────────────────────────────────────── auto& span = pText->text; if ( !span ) continue; @@ -480,14 +772,23 @@ void RichText::updateLayout() { if ( !mLines.empty() ) mLines.back().width += extraLeft; + // Shift curX inside to the left edge — text starts + // to the right of any left floats. + Float le = floatLeftEdge( curY ); + if ( curX < le ) + curX = le; + + // Narrow the available width by active floats at this Y. Uint32 textHints = span->getTextHints(); + Float effW = effectiveMaxWidthAt( curY ); + if ( mMaxWidth > 0 && mMaxWidth < effW ) + effW = mMaxWidth; - LineWrapInfoEx wrapInfo = LineWrap::computeLineBreaksEx( - span->getString(), fontStyle, mMaxWidth > 0 ? mMaxWidth : 1e9f, - mMaxWidth > 0 ? LineWrapMode::Word : LineWrapMode::NoWrap, false, 4, 0.f, textHints, - false, curX ); + LineWrapInfoEx wrapInfo = + LineWrap::computeLineBreaksEx( span->getString(), fontStyle, effW > 0 ? effW : 1e9f, + effW > 0 ? LineWrapMode::Word : LineWrapMode::NoWrap, + false, 4, 0.f, textHints, false, curX ); - // Make sure we have the end of the string as a "wrap" point for the loop if ( wrapInfo.wraps.empty() || wrapInfo.wraps.back() != (Float)span->getString().size() ) wrapInfo.wraps.push_back( span->getString().size() ); @@ -509,9 +810,8 @@ void RichText::updateLayout() { RenderSpan renderSpan; renderSpan.block = SpanBlock{ renderSpanText, pText->margin, pText->padding }; - renderSpan.position = { curX, 0 }; // Y adjusted later - renderSpan.size = - Sizef( spanWidth, height ); // Configured BEFORE pushing to vector + renderSpan.position = { curX, 0 }; + renderSpan.size = Sizef( spanWidth, height ); renderSpan.startCharIndex = curCharIdx; renderSpan.endCharIndex = curCharIdx + ( endIdx - startIdx ); curCharIdx = renderSpan.endCharIndex; @@ -526,22 +826,20 @@ void RichText::updateLayout() { currentLine.width += spanWidth; } + // Trailing margin may force a wrap. if ( i == wrapInfo.wraps.size() - 2 && !isNewline ) { Float extraRight = pText->margin.Right + pText->padding.Right; curX += extraRight; mLines.back().width += extraRight; - if ( !isNewline && mMaxWidth > 0 && curX > mMaxWidth ) { - // the margin forced a wrap + if ( effW > 0 && effW < 1e9f && curX > effW ) { maxWidth = std::max( maxWidth, curX ); mLines.push_back( RenderParagraph() ); curX = 0; - continue; // skip the next newline check + continue; } } - // If it's a newline, or if it's not the very last segment (which means it wrapped), - // start a new line. Exception: If the last segment was just a newline, we already - // handled it. + // Newline or soft-wrap → start a new line. if ( i < wrapInfo.wraps.size() - 2 || isNewline ) { if ( isNewline ) { curCharIdx++; @@ -556,52 +854,113 @@ void RichText::updateLayout() { curX = 0; } } - } else { // Drawable or CustomSize + } else { + // ── Drawable or CustomBlock ──────────────────────────── Sizef blockSize; bool isBlock = false; + UI::CSSFloat floatType = UI::CSSFloat::None; + UI::CSSClear clearType = UI::CSSClear::None; if ( auto pDrawable = std::get_if>( &block ) ) { auto& drawable = *pDrawable; blockSize = drawable ? drawable->getPixelsSize() : Sizef(); } else if ( auto pSize = std::get_if( &block ) ) { blockSize = pSize->size; isBlock = pSize->isBlock; + floatType = pSize->floatType; + clearType = pSize->clearType; } - if ( isBlock && curX > 0 ) { - maxWidth = std::max( maxWidth, curX ); - mLines.push_back( RenderParagraph() ); - curX = 0; + // ── Clear: advance curY past active floats ───────────── + if ( clearType != UI::CSSClear::None ) { + if ( clearFloats( clearType ) ) { + maxWidth = std::max( maxWidth, curX ); + mLines.push_back( RenderParagraph() ); + curX = 0; + } } - // Wrap if needed - if ( mMaxWidth > 0 && !isBlock && - ( curX + blockSize.getWidth() >= mMaxWidth || curX >= mMaxWidth ) && curX > 0 ) { - maxWidth = std::max( maxWidth, curX ); - mLines.push_back( RenderParagraph() ); - curX = 0; - } + // Left edge of open space at current Y (after any clears). + Float le = floatLeftEdge( curY ); - RenderSpan renderSpan; - renderSpan.block = block; - renderSpan.position = { curX, 0 }; - renderSpan.size = blockSize; - renderSpan.startCharIndex = curCharIdx; - renderSpan.endCharIndex = curCharIdx + 1; - curCharIdx = renderSpan.endCharIndex; + if ( floatType != UI::CSSFloat::None ) { + // ── Float placement ──────────────────────────────── + // Position the float at the left/right edge of the + // available space. Floats do NOT consume inline-flow + // horizontal space (curX is not advanced) and are not + // affected by text-align (see pass 2). + Float posX; + if ( floatType == UI::CSSFloat::Left ) { + posX = le; + } else { + Float re = floatRightEdge( curY ); + posX = re - blockSize.getWidth(); + if ( posX < le ) + posX = le; + } - RenderParagraph& currentLine = mLines.back(); - currentLine.spans.push_back( renderSpan ); + RenderSpan renderSpan; + renderSpan.block = block; + renderSpan.position = { posX, 0 }; + renderSpan.size = blockSize; + renderSpan.startCharIndex = curCharIdx; + renderSpan.endCharIndex = curCharIdx + 1; + curCharIdx = renderSpan.endCharIndex; - currentLine.maxAscent = std::max( currentLine.maxAscent, blockSize.getHeight() ); - currentLine.height = std::max( currentLine.height, blockSize.getHeight() ); + mLines.back().spans.push_back( renderSpan ); - curX += blockSize.getWidth(); - currentLine.width += blockSize.getWidth(); + // Record the float's bounding box so subsequent + // content can wrap around it. + Rectf fr( posX, curY, posX + blockSize.getWidth(), + curY + blockSize.getHeight() ); + if ( floatType == UI::CSSFloat::Left ) + leftFloats.push_back( fr ); + else + rightFloats.push_back( fr ); + } else { + // ── Normal (non-float) block ──────────────────── + if ( curX < le ) + curX = le; - if ( ( mMaxWidth > 0 && curX >= mMaxWidth ) || isBlock ) { - maxWidth = std::max( maxWidth, curX ); - mLines.push_back( RenderParagraph() ); - curX = 0; + // Block elements force a line break before. + if ( isBlock && curX > 0 ) { + maxWidth = std::max( maxWidth, curX ); + mLines.push_back( RenderParagraph() ); + curX = 0; + } + + // Wrap if the block doesn't fit in the available width + // (narrowed by active floats). + Float effW = effectiveMaxWidthAt( curY ); + if ( effW > 0 && effW < 1e9f && !isBlock && + ( curX + blockSize.getWidth() >= effW || curX >= effW ) && curX > 0 ) { + maxWidth = std::max( maxWidth, curX ); + mLines.push_back( RenderParagraph() ); + curX = 0; + } + + RenderSpan renderSpan; + renderSpan.block = block; + renderSpan.position = { curX, 0 }; + renderSpan.size = blockSize; + renderSpan.startCharIndex = curCharIdx; + renderSpan.endCharIndex = curCharIdx + 1; + curCharIdx = renderSpan.endCharIndex; + + RenderParagraph& currentLine = mLines.back(); + currentLine.spans.push_back( renderSpan ); + + currentLine.maxAscent = std::max( currentLine.maxAscent, blockSize.getHeight() ); + currentLine.height = std::max( currentLine.height, blockSize.getHeight() ); + + curX += blockSize.getWidth(); + currentLine.width += blockSize.getWidth(); + + // Block elements or overflow force a line break after. + if ( ( effW > 0 && effW < 1e9f && curX >= effW ) || isBlock ) { + maxWidth = std::max( maxWidth, curX ); + mLines.push_back( RenderParagraph() ); + curX = 0; + } } } } @@ -612,9 +971,12 @@ void RichText::updateLayout() { mLines.pop_back(); } - Float curY = 0; + // ── Pass 2: assign Y positions and apply text alignment ─────── + // NOTE: float spans are excluded from the xOffset because + // text-align only affects inline-flow content, not floated elements. + Float accumY = 0; for ( auto& line : mLines ) { - line.y = curY; + line.y = accumY; Float xOffset = 0; if ( mMaxWidth > 0 && mAlign != 0 ) { @@ -628,6 +990,11 @@ void RichText::updateLayout() { Float maxLineHeight = 0; for ( auto& span : line.spans ) { + bool isFloat = false; + if ( auto pSize = std::get_if( &span.block ) ) { + if ( pSize->floatType != UI::CSSFloat::None ) + isFloat = true; + } if ( auto pText = std::get_if( &span.block ) ) { auto& textBlock = pText->text; Float offsetY = line.maxAscent - textBlock->getCharacterSize(); @@ -638,17 +1005,19 @@ void RichText::updateLayout() { Float offsetY = line.maxAscent - span.size.getHeight(); if ( offsetY < 0 ) offsetY = 0; - span.position.x += xOffset; + // Float spans keep their edge-aligned x; only inline-flow spans shift. + if ( !isFloat ) + span.position.x += xOffset; span.position.y = offsetY; maxLineHeight = std::max( maxLineHeight, offsetY + span.size.getHeight() ); } } line.height = std::max( line.height, maxLineHeight ); - curY += line.height; + accumY += line.height; } - mSize = Sizef( maxWidth, curY ); + mSize = Sizef( maxWidth, accumY ); mTotalCharacterCount = curCharIdx; mNeedsLayoutUpdate = false; } diff --git a/src/eepp/graphics/text.cpp b/src/eepp/graphics/text.cpp index 712f54f99..9379b3d99 100644 --- a/src/eepp/graphics/text.cpp +++ b/src/eepp/graphics/text.cpp @@ -2309,7 +2309,7 @@ Uint32 Text::getNumLines() { return mString.countChar( '\n' ) + 1; } -const std::vector& Text::getLinesWidth() { +const SmallVector& Text::getLinesWidth() { cacheWidth(); return mLinesWidth; diff --git a/src/eepp/graphics/textlayout.cpp b/src/eepp/graphics/textlayout.cpp index b267d00e9..71dd466a0 100644 --- a/src/eepp/graphics/textlayout.cpp +++ b/src/eepp/graphics/textlayout.cpp @@ -538,8 +538,8 @@ TextLayout::Cache TextLayout::layout( const String& string, Font* font, const Ui keepIndentation, initialXOffset ); } -std::vector TextLayout::getLinesWidth() const { - std::vector lw; +SmallVector TextLayout::getLinesWidth() const { + SmallVector lw; std::size_t total = 0; for ( const auto& sp : paragraphs ) total += sp.wrapInfo.wrapsWidth.size(); @@ -570,7 +570,7 @@ void TextLayout::wrapLayout( const String::View& string, TextLayout& result, Sizef maxSize{ 0, vspace + yShift }; std::size_t startWrapsCount = sp.wrapInfo.wraps.size(); - std::vector wrapsWidth = std::move( sp.wrapInfo.wrapsWidth ); + auto wrapsWidth = std::move( sp.wrapInfo.wrapsWidth ); sp.wrapInfo.wrapsWidth.clear(); if ( keepIndentation && shapedGlyphCount ) { diff --git a/src/eepp/system/base64.cpp b/src/eepp/system/base64.cpp index 513155192..9bee1e5d5 100644 --- a/src/eepp/system/base64.cpp +++ b/src/eepp/system/base64.cpp @@ -7,7 +7,7 @@ namespace EE { namespace System { /* $Id: base64.c 156 2007-07-12 23:29:10Z orange $ */ /* decode a base64 string in one shot */ -int Base64::decode( size_t in_len, const char* in, size_t out_len, unsigned char* out ) { +size_t Base64::decode( size_t in_len, const char* in, size_t out_len, unsigned char* out ) { static const Uint8 base64dec_tab[256] = { 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, @@ -26,17 +26,18 @@ int Base64::decode( size_t in_len, const char* in, size_t out_len, unsigned char 255, 255, 255, 255, }; - unsigned ii, io; + size_t ii, io; Uint32 v; unsigned rem; for ( io = 0, ii = 0, v = 0, rem = 0; ii < in_len; ii++ ) { unsigned char ch; - if ( isspace( in[ii] ) ) + unsigned char c = (unsigned char)in[ii]; + if ( isspace( c ) ) continue; - if ( in[ii] == '=' ) + if ( c == '=' ) break; /* stop at = */ - ch = base64dec_tab[(unsigned)in[ii]]; + ch = base64dec_tab[c]; if ( ch == 255 ) break; /* stop at a parse error */ v = ( v << 6 ) | ch; @@ -57,11 +58,11 @@ int Base64::decode( size_t in_len, const char* in, size_t out_len, unsigned char return io; } -int Base64::encode( size_t in_len, const unsigned char* in, size_t out_len, char* out ) { +size_t Base64::encode( size_t in_len, const unsigned char* in, size_t out_len, char* out ) { static const Uint8 base64enc_tab[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; - unsigned ii, io; + size_t ii, io; Uint32 v; unsigned rem; @@ -94,14 +95,14 @@ int Base64::encode( size_t in_len, const unsigned char* in, size_t out_len, char return io; } -bool Base64::encode( const std::string& in, std::string& out ) { +bool Base64::encode( std::string_view in, std::string& out ) { size_t b64len = encodeSafeOutLen( in.size() ); if ( out.size() < b64len ) { out.resize( b64len ); } - int len = encode( in.size(), (const unsigned char*)in.c_str(), out.size(), (char*)&out[0] ); + int len = encode( in.size(), (const unsigned char*)in.data(), out.size(), (char*)&out[0] ); if ( -1 != len && (size_t)len != out.size() ) { out.resize( len ); @@ -110,20 +111,20 @@ bool Base64::encode( const std::string& in, std::string& out ) { return -1 != len; } -bool Base64::decode( const std::string& in, std::string& out ) { +size_t Base64::decode( std::string_view in, std::string& out ) { size_t d64len = decodeSafeOutLen( in.size() ); if ( out.size() < d64len ) { out.resize( d64len ); } - int len = decode( in.size(), in.c_str(), out.size(), (unsigned char*)&out[0] ); + int len = decode( in.size(), in.data(), out.size(), (unsigned char*)&out[0] ); if ( -1 != len && (size_t)len != out.size() ) { out.resize( len ); } - return -1 != len; + return len; } }} // namespace EE::System diff --git a/src/eepp/ui/css/drawableimageparser.cpp b/src/eepp/ui/css/drawableimageparser.cpp index 06f852692..cf0c2fb98 100644 --- a/src/eepp/ui/css/drawableimageparser.cpp +++ b/src/eepp/ui/css/drawableimageparser.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include #include @@ -331,12 +332,14 @@ void DrawableImageParser::registerBaseParsers() { UINode* node ) -> Drawable* { if ( functionType.getParameters().size() < 1 ) return NULL; - - return DrawableSearcher::searchByName( - node->getUISceneNode() - ->solveRelativePath( functionType.getParameters().at( 0 ) ) - .toString(), - false, node->getUISceneNode()->getReferer() ); + const auto& param = functionType.getParameters().at( 0 ); + if ( functionType.getName() == "url" && !param.empty() && param[0] != '@' && + !String::startsWith( param, "data:image/" ) ) { + return DrawableSearcher::searchByName( + node->getUISceneNode()->solveRelativePath( param ).toString(), false, + node->getUISceneNode()->getReferer() ); + } + return DrawableSearcher::searchByName( param, false, node->getUISceneNode()->getReferer() ); }; mFuncs["icon"] = []( const FunctionString& functionType, const Sizef& size, bool&, diff --git a/src/eepp/ui/css/stylesheetspecification.cpp b/src/eepp/ui/css/stylesheetspecification.cpp index 5d67cb5e3..9329c5a1c 100644 --- a/src/eepp/ui/css/stylesheetspecification.cpp +++ b/src/eepp/ui/css/stylesheetspecification.cpp @@ -433,6 +433,8 @@ void StyleSheetSpecification::registerDefaultProperties() { registerProperty( "hidden", "" ).setType( PropertyType::Bool ); registerProperty( "display", "inline" ).setType( PropertyType::String ); registerProperty( "position", "static" ).setType( PropertyType::String ); + registerProperty( "float", "none" ).setType( PropertyType::String ); + registerProperty( "clear", "none" ).setType( PropertyType::String ); registerProperty( "list-style-type", "none", true ).setType( PropertyType::String ); registerProperty( "list-style-position", "outside", true ).setType( PropertyType::String ); registerProperty( "list-style-image", "none" ).setType( PropertyType::String ); @@ -478,6 +480,7 @@ void StyleSheetSpecification::registerDefaultProperties() { registerProperty( "method", "GET" ).setType( PropertyType::String ); registerProperty( "enctype", "application/x-www-form-urlencoded" ) .setType( PropertyType::String ); + registerProperty( "target", "_self" ).setType( PropertyType::String ); // Shorthands registerShorthand( "margin", { "margin-top", "margin-right", "margin-bottom", "margin-left" }, @@ -1006,7 +1009,10 @@ void StyleSheetSpecification::registerDefaultShorthandParsers() { std::string positionStr; for ( auto& tok : tokens ) { - if ( mDrawableImageParser.exists( tok ) ) { + auto open = tok.find_first_of( '(' ); + + if ( open != std::string::npos && + mDrawableImageParser.exists( tok.substr( 0, open ) ) ) { int pos = getIndexEndingWith( propNames, "-image" ); if ( pos != -1 ) properties.emplace_back( StyleSheetProperty( propNames[pos], tok ) ); diff --git a/src/eepp/ui/uihtmlwidget.cpp b/src/eepp/ui/uihtmlwidget.cpp index ada6f9ef8..dc1b3de39 100644 --- a/src/eepp/ui/uihtmlwidget.cpp +++ b/src/eepp/ui/uihtmlwidget.cpp @@ -69,6 +69,20 @@ void UIHTMLWidget::setCSSPosition( CSSPosition position ) { } } +void UIHTMLWidget::setCSSFloat( CSSFloat cssFloat ) { + if ( mFloat != cssFloat ) { + mFloat = cssFloat; + notifyLayoutAttrChange(); + } +} + +void UIHTMLWidget::setCSSClear( CSSClear cssClear ) { + if ( mClear != cssClear ) { + mClear = cssClear; + notifyLayoutAttrChange(); + } +} + void UIHTMLWidget::setOffsets( const Rectf& offsets ) { if ( mOffsets != offsets ) { mOffsets = offsets; @@ -86,7 +100,8 @@ void UIHTMLWidget::setZIndex( int zIndex ) { std::vector UIHTMLWidget::getPropertiesImplemented() const { auto props = UILayout::getPropertiesImplemented(); - auto local = { PropertyId::Display, PropertyId::Position, PropertyId::Top, PropertyId::Right, + auto local = { PropertyId::Display, PropertyId::Position, PropertyId::Float, + PropertyId::Clear, PropertyId::Top, PropertyId::Right, PropertyId::Bottom, PropertyId::Left, PropertyId::ZIndex }; props.insert( props.end(), local.begin(), local.end() ); return props; @@ -102,6 +117,10 @@ std::string UIHTMLWidget::getPropertyString( const PropertyDefinition* propertyD return CSSDisplayHelper::toString( mDisplay ); case PropertyId::Position: return CSSPositionHelper::toString( mPosition ); + case PropertyId::Float: + return CSSFloatHelper::toString( mFloat ); + case PropertyId::Clear: + return CSSClearHelper::toString( mClear ); case PropertyId::Top: return mTopEq; case PropertyId::Right: @@ -130,6 +149,14 @@ bool UIHTMLWidget::applyProperty( const StyleSheetProperty& attribute ) { setCSSPosition( CSSPositionHelper::fromString( attribute.asString() ) ); return true; } + case PropertyId::Float: { + setCSSFloat( CSSFloatHelper::fromString( attribute.asString() ) ); + return true; + } + case PropertyId::Clear: { + setCSSClear( CSSClearHelper::fromString( attribute.asString() ) ); + return true; + } case PropertyId::ZIndex: { setZIndex( attribute.asInt() ); return true; diff --git a/src/eepp/ui/uirichtext.cpp b/src/eepp/ui/uirichtext.cpp index 0145460f3..1a703b953 100644 --- a/src/eepp/ui/uirichtext.cpp +++ b/src/eepp/ui/uirichtext.cpp @@ -737,9 +737,16 @@ void UIRichText::rebuildRichText( UILayout* container, RichText& richText, Intri margin.Right ); } + CSSFloat floatType = CSSFloat::None; + CSSClear clearType = CSSClear::None; + if ( widget->isType( UI_TYPE_HTML_WIDGET ) ) { + floatType = widget->asType()->getCSSFloat(); + clearType = widget->asType()->getCSSClear(); + } + richText.addCustomSize( Sizef( w + margin.Left + margin.Right, size.getHeight() + margin.Top + margin.Bottom ), - isBlock ); + isBlock, floatType, clearType ); } }; diff --git a/src/eepp/ui/uitextspan.cpp b/src/eepp/ui/uitextspan.cpp index fc16cbc67..6d280d036 100644 --- a/src/eepp/ui/uitextspan.cpp +++ b/src/eepp/ui/uitextspan.cpp @@ -623,6 +623,10 @@ bool UIAnchorSpan::applyProperty( const StyleSheetProperty& attribute ) { return false; switch ( attribute.getPropertyDefinition()->getPropertyId() ) { + case PropertyId::Target:{ + mTarget = attribute.value(); + break; + } case PropertyId::Href: setHref( attribute.asString() ); break; @@ -661,6 +665,8 @@ std::string UIAnchorSpan::getPropertyString( const PropertyDefinition* propertyD return ""; switch ( propertyDef->getPropertyId() ) { + case PropertyId::Target: + return mTarget; case PropertyId::Href: return mHref; default: @@ -670,7 +676,7 @@ std::string UIAnchorSpan::getPropertyString( const PropertyDefinition* propertyD std::vector UIAnchorSpan::getPropertiesImplemented() const { auto props = UITextSpan::getPropertiesImplemented(); - auto local = { PropertyId::Href }; + auto local = { PropertyId::Href, PropertyId::Target }; props.insert( props.end(), local.begin(), local.end() ); return props; } diff --git a/src/tests/unit_tests/uihtml_float_tests.cpp b/src/tests/unit_tests/uihtml_float_tests.cpp new file mode 100644 index 000000000..45518d50d --- /dev/null +++ b/src/tests/unit_tests/uihtml_float_tests.cpp @@ -0,0 +1,607 @@ +#include "utest.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace EE; +using namespace EE::UI; +using namespace EE::Window; +using namespace EE::Graphics; + +static void init_float_test() { + Engine::instance()->createWindow( + WindowSettings( 800, 600, "Float Layout Test", WindowStyle::Default, WindowBackend::Default, + 32, {}, 1, false, true ) ); + FileSystem::changeWorkingDirectory( Sys::getProcessPath() ); + + FontTrueType* font = FontTrueType::New( "NotoSans-Regular" ); + font->loadFromFile( "../assets/fonts/NotoSans-Regular.ttf" ); + FontFamily::loadFromRegular( font ); + + UI::UISceneNode* sceneNode = UI::UISceneNode::New(); + SceneManager::instance()->add( sceneNode ); + UI::UIThemeManager* themeManager = sceneNode->getUIThemeManager(); + themeManager->setDefaultFont( font ); +} + +UTEST( UIHTMLFloat, structure_FloatAndClearEnums ) { + EXPECT_TRUE( CSSFloatHelper::toString( CSSFloat::None ) == "none" ); + EXPECT_TRUE( CSSFloatHelper::toString( CSSFloat::Left ) == "left" ); + EXPECT_TRUE( CSSFloatHelper::toString( CSSFloat::Right ) == "right" ); + + EXPECT_EQ( (int)CSSFloat::None, (int)CSSFloatHelper::fromString( "none" ) ); + EXPECT_EQ( (int)CSSFloat::Left, (int)CSSFloatHelper::fromString( "left" ) ); + EXPECT_EQ( (int)CSSFloat::Right, (int)CSSFloatHelper::fromString( "right" ) ); + EXPECT_EQ( (int)CSSFloat::None, (int)CSSFloatHelper::fromString( "invalid" ) ); + + EXPECT_TRUE( CSSClearHelper::toString( CSSClear::None ) == "none" ); + EXPECT_TRUE( CSSClearHelper::toString( CSSClear::Left ) == "left" ); + EXPECT_TRUE( CSSClearHelper::toString( CSSClear::Right ) == "right" ); + EXPECT_TRUE( CSSClearHelper::toString( CSSClear::Both ) == "both" ); + + EXPECT_EQ( (int)CSSClear::None, (int)CSSClearHelper::fromString( "none" ) ); + EXPECT_EQ( (int)CSSClear::Left, (int)CSSClearHelper::fromString( "left" ) ); + EXPECT_EQ( (int)CSSClear::Right, (int)CSSClearHelper::fromString( "right" ) ); + EXPECT_EQ( (int)CSSClear::Both, (int)CSSClearHelper::fromString( "both" ) ); + EXPECT_EQ( (int)CSSClear::None, (int)CSSClearHelper::fromString( "garbage" ) ); +} + +UTEST( UIHTMLFloat, property_DefaultsAreNone ) { + UIHTMLWidget* w = UIHTMLWidget::New(); + EXPECT_EQ( CSSFloat::None, w->getCSSFloat() ); + EXPECT_EQ( CSSClear::None, w->getCSSClear() ); + eeDelete( w ); +} + +UTEST( UIHTMLFloat, property_SetFloatViaApplyProperty ) { + UIHTMLWidget* w = UIHTMLWidget::New(); + w->applyProperty( StyleSheetProperty( "float", "left" ) ); + EXPECT_EQ( CSSFloat::Left, w->getCSSFloat() ); + w->applyProperty( StyleSheetProperty( "float", "right" ) ); + EXPECT_EQ( CSSFloat::Right, w->getCSSFloat() ); + w->applyProperty( StyleSheetProperty( "float", "none" ) ); + EXPECT_EQ( CSSFloat::None, w->getCSSFloat() ); + eeDelete( w ); +} + +UTEST( UIHTMLFloat, property_SetClearViaApplyProperty ) { + UIHTMLWidget* w = UIHTMLWidget::New(); + w->applyProperty( StyleSheetProperty( "clear", "left" ) ); + EXPECT_EQ( CSSClear::Left, w->getCSSClear() ); + w->applyProperty( StyleSheetProperty( "clear", "right" ) ); + EXPECT_EQ( CSSClear::Right, w->getCSSClear() ); + w->applyProperty( StyleSheetProperty( "clear", "both" ) ); + EXPECT_EQ( CSSClear::Both, w->getCSSClear() ); + w->applyProperty( StyleSheetProperty( "clear", "none" ) ); + EXPECT_EQ( CSSClear::None, w->getCSSClear() ); + eeDelete( w ); +} + +UTEST( UIHTMLFloat, property_GetPropertyString ) { + UIHTMLWidget* w = UIHTMLWidget::New(); + w->setCSSFloat( CSSFloat::Left ); + w->setCSSClear( CSSClear::Right ); + auto props = w->getPropertiesImplemented(); + bool hasFloat = false, hasClear = false; + for ( auto& p : props ) { + if ( p == PropertyId::Float ) + hasFloat = true; + if ( p == PropertyId::Clear ) + hasClear = true; + } + EXPECT_TRUE( hasFloat ); + EXPECT_TRUE( hasClear ); + eeDelete( w ); +} + +UTEST( UIHTMLFloat, richtext_NoFloatLayout_NoChange ) { + init_float_test(); + UISceneNode* sceneNode = SceneManager::instance()->getUISceneNode(); + + UIRichText* container = UIRichText::New(); + container->setParent( sceneNode->getRoot() ); + container->setPixelsSize( 600, 400 ); + container->setPixelsPosition( 10, 10 ); + container->setLayoutSizePolicy( SizePolicy::Fixed, SizePolicy::WrapContent ); + + UIHTMLWidget* child1 = UIHTMLWidget::New(); + child1->setParent( container ); + child1->setPixelsSize( 100, 50 ); + child1->setLayoutSizePolicy( SizePolicy::Fixed, SizePolicy::Fixed ); + + UIHTMLWidget* child2 = UIHTMLWidget::New(); + child2->setParent( container ); + child2->setPixelsSize( 150, 30 ); + child2->setLayoutSizePolicy( SizePolicy::Fixed, SizePolicy::Fixed ); + + sceneNode->updateDirtyLayouts(); + + Vector2f pos1 = child1->convertToWorldSpace( { 0, 0 } ); + Vector2f pos2 = child2->convertToWorldSpace( { 0, 0 } ); + + EXPECT_GE( pos2.x, pos1.x + child1->getPixelsSize().getWidth() - 1.f ); + + Engine::destroySingleton(); +} + +UTEST( UIHTMLFloat, floatLeft_TextWrapsRight ) { + init_float_test(); + UISceneNode* sceneNode = SceneManager::instance()->getUISceneNode(); + + UIRichText* container = UIRichText::New(); + container->setParent( sceneNode->getRoot() ); + container->setPixelsSize( 600, 400 ); + container->setPixelsPosition( 10, 10 ); + container->setLayoutSizePolicy( SizePolicy::Fixed, SizePolicy::WrapContent ); + + UIHTMLWidget* floatChild = UIHTMLWidget::New(); + floatChild->setParent( container ); + floatChild->setPixelsSize( 100, 50 ); + floatChild->setCSSFloat( CSSFloat::Left ); + floatChild->setLayoutSizePolicy( SizePolicy::Fixed, SizePolicy::Fixed ); + + UIHTMLWidget* inlineChild = UIHTMLWidget::New(); + inlineChild->setParent( container ); + inlineChild->setPixelsSize( 80, 30 ); + inlineChild->setLayoutSizePolicy( SizePolicy::Fixed, SizePolicy::Fixed ); + + sceneNode->updateDirtyLayouts(); + + Vector2f fpos = floatChild->convertToWorldSpace( { 0, 0 } ); + Vector2f ipos = inlineChild->convertToWorldSpace( { 0, 0 } ); + + EXPECT_NEAR( fpos.y, ipos.y, 1.f ); + EXPECT_GE( ipos.x, fpos.x + floatChild->getPixelsSize().getWidth() - 1.f ); + + Engine::destroySingleton(); +} + +UTEST( UIHTMLFloat, floatRight_TextFlowsLeft ) { + init_float_test(); + UISceneNode* sceneNode = SceneManager::instance()->getUISceneNode(); + + UIRichText* container = UIRichText::New(); + container->setParent( sceneNode->getRoot() ); + container->setPixelsSize( 600, 400 ); + container->setPixelsPosition( 10, 10 ); + container->setLayoutSizePolicy( SizePolicy::Fixed, SizePolicy::WrapContent ); + + UIHTMLWidget* floatChild = UIHTMLWidget::New(); + floatChild->setParent( container ); + floatChild->setPixelsSize( 100, 50 ); + floatChild->setCSSFloat( CSSFloat::Right ); + floatChild->setLayoutSizePolicy( SizePolicy::Fixed, SizePolicy::Fixed ); + + UIHTMLWidget* inlineChild = UIHTMLWidget::New(); + inlineChild->setParent( container ); + inlineChild->setPixelsSize( 80, 30 ); + inlineChild->setLayoutSizePolicy( SizePolicy::Fixed, SizePolicy::Fixed ); + + sceneNode->updateDirtyLayouts(); + + Vector2f fpos = floatChild->convertToWorldSpace( { 0, 0 } ); + Vector2f ipos = inlineChild->convertToWorldSpace( { 0, 0 } ); + + EXPECT_NEAR( fpos.y, ipos.y, 1.f ); + Float fRightEdge = fpos.x + floatChild->getPixelsSize().getWidth(); + EXPECT_LT( ipos.x + inlineChild->getPixelsSize().getWidth(), fRightEdge + 1.f ); + + Engine::destroySingleton(); +} + +UTEST( UIHTMLFloat, twoFloatsLeft_StackHorizontally ) { + init_float_test(); + UISceneNode* sceneNode = SceneManager::instance()->getUISceneNode(); + + UIRichText* container = UIRichText::New(); + container->setParent( sceneNode->getRoot() ); + container->setPixelsSize( 600, 400 ); + container->setPixelsPosition( 10, 10 ); + container->setLayoutSizePolicy( SizePolicy::Fixed, SizePolicy::WrapContent ); + + UIHTMLWidget* float1 = UIHTMLWidget::New(); + float1->setParent( container ); + float1->setPixelsSize( 100, 50 ); + float1->setCSSFloat( CSSFloat::Left ); + float1->setLayoutSizePolicy( SizePolicy::Fixed, SizePolicy::Fixed ); + + UIHTMLWidget* float2 = UIHTMLWidget::New(); + float2->setParent( container ); + float2->setPixelsSize( 120, 40 ); + float2->setCSSFloat( CSSFloat::Left ); + float2->setLayoutSizePolicy( SizePolicy::Fixed, SizePolicy::Fixed ); + + sceneNode->updateDirtyLayouts(); + + Vector2f f1pos = float1->convertToWorldSpace( { 0, 0 } ); + Vector2f f2pos = float2->convertToWorldSpace( { 0, 0 } ); + + EXPECT_NEAR( f1pos.y, f2pos.y, 1.f ); + EXPECT_NEAR( f2pos.x, f1pos.x + 100.f, 1.f ); + + Engine::destroySingleton(); +} + +UTEST( UIHTMLFloat, twoFloatsRight_StackHorizontally ) { + init_float_test(); + UISceneNode* sceneNode = SceneManager::instance()->getUISceneNode(); + + UIRichText* container = UIRichText::New(); + container->setParent( sceneNode->getRoot() ); + container->setPixelsSize( 600, 400 ); + container->setPixelsPosition( 10, 10 ); + container->setLayoutSizePolicy( SizePolicy::Fixed, SizePolicy::WrapContent ); + + UIHTMLWidget* float1 = UIHTMLWidget::New(); + float1->setParent( container ); + float1->setPixelsSize( 100, 50 ); + float1->setCSSFloat( CSSFloat::Right ); + float1->setLayoutSizePolicy( SizePolicy::Fixed, SizePolicy::Fixed ); + + UIHTMLWidget* float2 = UIHTMLWidget::New(); + float2->setParent( container ); + float2->setPixelsSize( 80, 40 ); + float2->setCSSFloat( CSSFloat::Right ); + float2->setLayoutSizePolicy( SizePolicy::Fixed, SizePolicy::Fixed ); + + sceneNode->updateDirtyLayouts(); + + Vector2f f1pos = float1->convertToWorldSpace( { 0, 0 } ); + Vector2f f2pos = float2->convertToWorldSpace( { 0, 0 } ); + + EXPECT_NEAR( f1pos.y, f2pos.y, 1.f ); + EXPECT_GT( f1pos.x, f2pos.x ); + + Engine::destroySingleton(); +} + +UTEST( UIHTMLFloat, clearBoth_JumpsBelowAllFloats ) { + init_float_test(); + UISceneNode* sceneNode = SceneManager::instance()->getUISceneNode(); + + UIRichText* container = UIRichText::New(); + container->setParent( sceneNode->getRoot() ); + container->setPixelsSize( 600, 400 ); + container->setPixelsPosition( 10, 10 ); + container->setLayoutSizePolicy( SizePolicy::Fixed, SizePolicy::WrapContent ); + + UIHTMLWidget* floatLeft = UIHTMLWidget::New(); + floatLeft->setParent( container ); + floatLeft->setPixelsSize( 100, 80 ); + floatLeft->setCSSFloat( CSSFloat::Left ); + floatLeft->setLayoutSizePolicy( SizePolicy::Fixed, SizePolicy::Fixed ); + + UIHTMLWidget* floatRight = UIHTMLWidget::New(); + floatRight->setParent( container ); + floatRight->setPixelsSize( 90, 60 ); + floatRight->setCSSFloat( CSSFloat::Right ); + floatRight->setLayoutSizePolicy( SizePolicy::Fixed, SizePolicy::Fixed ); + + UIHTMLWidget* clearChild = UIHTMLWidget::New(); + clearChild->setParent( container ); + clearChild->setPixelsSize( 200, 30 ); + clearChild->setCSSClear( CSSClear::Both ); + clearChild->setLayoutSizePolicy( SizePolicy::Fixed, SizePolicy::Fixed ); + + sceneNode->updateDirtyLayouts(); + + Vector2f fLeftPos = floatLeft->convertToWorldSpace( { 0, 0 } ); + Vector2f fRightPos = floatRight->convertToWorldSpace( { 0, 0 } ); + Vector2f clearPos = clearChild->convertToWorldSpace( { 0, 0 } ); + + EXPECT_GE( clearPos.y, fLeftPos.y + floatLeft->getPixelsSize().getHeight() - 1.f ); + EXPECT_GE( clearPos.y, fRightPos.y + floatRight->getPixelsSize().getHeight() - 1.f ); + + Engine::destroySingleton(); +} + +UTEST( UIHTMLFloat, clearLeft_OnlyJumpsPastLeftFloats ) { + init_float_test(); + UISceneNode* sceneNode = SceneManager::instance()->getUISceneNode(); + + UIRichText* container = UIRichText::New(); + container->setParent( sceneNode->getRoot() ); + container->setPixelsSize( 600, 400 ); + container->setPixelsPosition( 10, 10 ); + container->setLayoutSizePolicy( SizePolicy::Fixed, SizePolicy::WrapContent ); + + UIHTMLWidget* floatLeft = UIHTMLWidget::New(); + floatLeft->setParent( container ); + floatLeft->setPixelsSize( 100, 120 ); + floatLeft->setCSSFloat( CSSFloat::Left ); + floatLeft->setLayoutSizePolicy( SizePolicy::Fixed, SizePolicy::Fixed ); + + UIHTMLWidget* inlineChild = UIHTMLWidget::New(); + inlineChild->setParent( container ); + inlineChild->setPixelsSize( 50, 20 ); + inlineChild->setLayoutSizePolicy( SizePolicy::Fixed, SizePolicy::Fixed ); + + UIHTMLWidget* clearLeftChild = UIHTMLWidget::New(); + clearLeftChild->setParent( container ); + clearLeftChild->setPixelsSize( 200, 30 ); + clearLeftChild->setCSSClear( CSSClear::Left ); + clearLeftChild->setLayoutSizePolicy( SizePolicy::Fixed, SizePolicy::Fixed ); + + sceneNode->updateDirtyLayouts(); + + Vector2f floatPos = floatLeft->convertToWorldSpace( { 0, 0 } ); + Vector2f clearPos = clearLeftChild->convertToWorldSpace( { 0, 0 } ); + + EXPECT_GE( clearPos.y, floatPos.y + floatLeft->getPixelsSize().getHeight() - 1.f ); + + Engine::destroySingleton(); +} + +UTEST( UIHTMLFloat, clearRight_RespectsRightFloats ) { + init_float_test(); + UISceneNode* sceneNode = SceneManager::instance()->getUISceneNode(); + + UIRichText* container = UIRichText::New(); + container->setParent( sceneNode->getRoot() ); + container->setPixelsSize( 600, 400 ); + container->setPixelsPosition( 10, 10 ); + container->setLayoutSizePolicy( SizePolicy::Fixed, SizePolicy::WrapContent ); + + UIHTMLWidget* floatRight = UIHTMLWidget::New(); + floatRight->setParent( container ); + floatRight->setPixelsSize( 100, 100 ); + floatRight->setCSSFloat( CSSFloat::Right ); + floatRight->setLayoutSizePolicy( SizePolicy::Fixed, SizePolicy::Fixed ); + + UIHTMLWidget* clearRightChild = UIHTMLWidget::New(); + clearRightChild->setParent( container ); + clearRightChild->setPixelsSize( 200, 30 ); + clearRightChild->setCSSClear( CSSClear::Right ); + clearRightChild->setLayoutSizePolicy( SizePolicy::Fixed, SizePolicy::Fixed ); + + sceneNode->updateDirtyLayouts(); + + Vector2f fpos = floatRight->convertToWorldSpace( { 0, 0 } ); + Vector2f clearPos = clearRightChild->convertToWorldSpace( { 0, 0 } ); + + EXPECT_GE( clearPos.y, fpos.y + floatRight->getPixelsSize().getHeight() - 1.f ); + + Engine::destroySingleton(); +} + +UTEST( UIHTMLFloat, mixedLeftRight_ContentBetween ) { + init_float_test(); + UISceneNode* sceneNode = SceneManager::instance()->getUISceneNode(); + + UIRichText* container = UIRichText::New(); + container->setParent( sceneNode->getRoot() ); + container->setPixelsSize( 600, 400 ); + container->setPixelsPosition( 10, 10 ); + container->setLayoutSizePolicy( SizePolicy::Fixed, SizePolicy::WrapContent ); + + UIHTMLWidget* floatLeft = UIHTMLWidget::New(); + floatLeft->setParent( container ); + floatLeft->setPixelsSize( 100, 50 ); + floatLeft->setCSSFloat( CSSFloat::Left ); + floatLeft->setLayoutSizePolicy( SizePolicy::Fixed, SizePolicy::Fixed ); + + UIHTMLWidget* floatRight = UIHTMLWidget::New(); + floatRight->setParent( container ); + floatRight->setPixelsSize( 80, 50 ); + floatRight->setCSSFloat( CSSFloat::Right ); + floatRight->setLayoutSizePolicy( SizePolicy::Fixed, SizePolicy::Fixed ); + + UIHTMLWidget* middleChild = UIHTMLWidget::New(); + middleChild->setParent( container ); + middleChild->setPixelsSize( 150, 30 ); + middleChild->setLayoutSizePolicy( SizePolicy::Fixed, SizePolicy::Fixed ); + + sceneNode->updateDirtyLayouts(); + + Vector2f fLeftPos = floatLeft->convertToWorldSpace( { 0, 0 } ); + Vector2f fRightPos = floatRight->convertToWorldSpace( { 0, 0 } ); + Vector2f midPos = middleChild->convertToWorldSpace( { 0, 0 } ); + + EXPECT_NEAR( fLeftPos.y, fRightPos.y, 1.f ); + EXPECT_NEAR( fLeftPos.y, midPos.y, 1.f ); + + EXPECT_GE( midPos.x, fLeftPos.x + floatLeft->getPixelsSize().getWidth() - 1.f ); + EXPECT_LE( midPos.x + middleChild->getPixelsSize().getWidth(), fRightPos.x + 1.f ); + + Engine::destroySingleton(); +} + +UTEST( UIHTMLFloat, floatWrapsContentBelowWhenTooWide ) { + init_float_test(); + UISceneNode* sceneNode = SceneManager::instance()->getUISceneNode(); + + UIRichText* container = UIRichText::New(); + container->setParent( sceneNode->getRoot() ); + container->setPixelsSize( 600, 400 ); + container->setPixelsPosition( 10, 10 ); + container->setLayoutSizePolicy( SizePolicy::Fixed, SizePolicy::WrapContent ); + + UIHTMLWidget* floatLeft = UIHTMLWidget::New(); + floatLeft->setParent( container ); + floatLeft->setPixelsSize( 350, 30 ); + floatLeft->setCSSFloat( CSSFloat::Left ); + floatLeft->setLayoutSizePolicy( SizePolicy::Fixed, SizePolicy::Fixed ); + + UIHTMLWidget* wideChild = UIHTMLWidget::New(); + wideChild->setParent( container ); + wideChild->setPixelsSize( 400, 25 ); + wideChild->setLayoutSizePolicy( SizePolicy::Fixed, SizePolicy::Fixed ); + + sceneNode->updateDirtyLayouts(); + + Vector2f widePos = wideChild->convertToWorldSpace( { 0, 0 } ); + Vector2f fpos = floatLeft->convertToWorldSpace( { 0, 0 } ); + + EXPECT_GT( widePos.y, fpos.y + 1.f ); + + Engine::destroySingleton(); +} + +UTEST( UIHTMLFloat, floatLeft_InlineBlockBeside ) { + init_float_test(); + UISceneNode* sceneNode = SceneManager::instance()->getUISceneNode(); + + UIRichText* container = UIRichText::New(); + container->setParent( sceneNode->getRoot() ); + container->setPixelsSize( 600, 400 ); + container->setPixelsPosition( 10, 10 ); + container->setLayoutSizePolicy( SizePolicy::Fixed, SizePolicy::WrapContent ); + + UIHTMLWidget* floatLeft = UIHTMLWidget::New(); + floatLeft->setParent( container ); + floatLeft->setPixelsSize( 100, 50 ); + floatLeft->setCSSFloat( CSSFloat::Left ); + floatLeft->setLayoutSizePolicy( SizePolicy::Fixed, SizePolicy::Fixed ); + + UIHTMLWidget* inlineBlock = UIHTMLWidget::New(); + inlineBlock->setParent( container ); + inlineBlock->setPixelsSize( 80, 30 ); + inlineBlock->setLayoutSizePolicy( SizePolicy::Fixed, SizePolicy::Fixed ); + + sceneNode->updateDirtyLayouts(); + + Vector2f fpos = floatLeft->convertToWorldSpace( { 0, 0 } ); + Vector2f ipos = inlineBlock->convertToWorldSpace( { 0, 0 } ); + + EXPECT_NEAR( fpos.y, ipos.y, 1.f ); + EXPECT_GE( ipos.x, fpos.x + floatLeft->getPixelsSize().getWidth() - 1.f ); + + Engine::destroySingleton(); +} + +UTEST( UIHTMLFloat, floatLeft_LargeFloat_PushesContentDown ) { + init_float_test(); + UISceneNode* sceneNode = SceneManager::instance()->getUISceneNode(); + + UIRichText* container = UIRichText::New(); + container->setParent( sceneNode->getRoot() ); + container->setPixelsSize( 600, 400 ); + container->setPixelsPosition( 10, 10 ); + container->setLayoutSizePolicy( SizePolicy::Fixed, SizePolicy::WrapContent ); + + UIHTMLWidget* floatLeft = UIHTMLWidget::New(); + floatLeft->setParent( container ); + floatLeft->setPixelsSize( 200, 120 ); + floatLeft->setCSSFloat( CSSFloat::Left ); + floatLeft->setLayoutSizePolicy( SizePolicy::Fixed, SizePolicy::Fixed ); + + UIHTMLWidget* afterFloat = UIHTMLWidget::New(); + afterFloat->setParent( container ); + afterFloat->setPixelsSize( 200, 30 ); + afterFloat->setCSSClear( CSSClear::Both ); + afterFloat->setLayoutSizePolicy( SizePolicy::Fixed, SizePolicy::Fixed ); + + sceneNode->updateDirtyLayouts(); + + Vector2f fpos = floatLeft->convertToWorldSpace( { 0, 0 } ); + Vector2f afterPos = afterFloat->convertToWorldSpace( { 0, 0 } ); + + EXPECT_GE( afterPos.y, fpos.y + floatLeft->getPixelsSize().getHeight() - 1.f ); + + Engine::destroySingleton(); +} + +UTEST( UIHTMLFloat, floatLeftNonHTMLwidget_NoCrash ) { + init_float_test(); + UISceneNode* sceneNode = SceneManager::instance()->getUISceneNode(); + + UIRichText* container = UIRichText::New(); + container->setParent( sceneNode->getRoot() ); + container->setPixelsSize( 600, 400 ); + container->setPixelsPosition( 10, 10 ); + container->setLayoutSizePolicy( SizePolicy::Fixed, SizePolicy::WrapContent ); + + UIWidget* plainWidget = UIWidget::New(); + plainWidget->setParent( container ); + plainWidget->setPixelsSize( 100, 50 ); + plainWidget->setLayoutSizePolicy( SizePolicy::Fixed, SizePolicy::Fixed ); + + UIWidget* plainWidget2 = UIWidget::New(); + plainWidget2->setParent( container ); + plainWidget2->setPixelsSize( 80, 30 ); + plainWidget2->setLayoutSizePolicy( SizePolicy::Fixed, SizePolicy::Fixed ); + + sceneNode->updateDirtyLayouts(); + + Vector2f pos1 = plainWidget->convertToWorldSpace( { 0, 0 } ); + Vector2f pos2 = plainWidget2->convertToWorldSpace( { 0, 0 } ); + + EXPECT_GE( pos2.x, pos1.x + plainWidget->getPixelsSize().getWidth() - 1.f ); + + Engine::destroySingleton(); +} + +UTEST( UIHTMLFloat, floatNotAffectedByTextAlignCenter ) { + Engine::instance()->createWindow( + WindowSettings( 800, 600, "Float + TextAlign Test", WindowStyle::Default, + WindowBackend::Default, 32, {}, 1, false, true ), + ContextSettings( false, 0, 0, GLv_default, true, false ) ); + FileSystem::changeWorkingDirectory( Sys::getProcessPath() ); + + FontTrueType* font = FontTrueType::New( "NotoSans-Regular" ); + font->loadFromFile( "../assets/fonts/NotoSans-Regular.ttf" ); + FontFamily::loadFromRegular( font ); + + UI::UISceneNode* sceneNode = UI::UISceneNode::New(); + SceneManager::instance()->add( sceneNode ); + UI::UIThemeManager* themeManager = sceneNode->getUIThemeManager(); + themeManager->setDefaultFont( font ); + + sceneNode->setURI( "file://" + Sys::getProcessPath() + "assets/html/" ); + std::string html; + FileSystem::fileGet( "assets/html/position_absolute_and_float.html", html ); + sceneNode->loadLayoutFromString( UI::Tools::HTMLFormatter::HTMLtoXML( html ) ); + + sceneNode->update( Milliseconds( 16 ) ); + sceneNode->updateDirtyLayouts(); + + UIWidget* mainWidget = sceneNode->getRoot()->find( "main" ); + ASSERT_TRUE( mainWidget != nullptr ); + + // The "main" div has two children with class "box" + // Each "box" has float:left, clear:both, text-align:center + // Inside the first box: .titlebox (float:left) and .login_inbox (float:left) + Node* child = mainWidget->getFirstChild(); + UIWidget* firstBox = nullptr; + while ( child ) { + if ( child->isWidget() ) { + UIWidget* w = child->asType(); + if ( w->isType( UI_TYPE_HTML_WIDGET ) && + w->asType()->getCSSFloat() == CSSFloat::Left ) { + firstBox = w; + break; + } + } + child = child->getNextNode(); + } + ASSERT_TRUE( firstBox != nullptr ); + + // The box's children (float:left) should not be shifted by text-align:center + Vector2f boxOrigin = firstBox->convertToWorldSpace( { 0, 0 } ); + + Node* boxChild = firstBox->getFirstChild(); + while ( boxChild ) { + if ( boxChild->isWidget() ) { + UIWidget* bc = boxChild->asType(); + if ( bc->isType( UI_TYPE_HTML_WIDGET ) && + bc->asType()->getCSSFloat() == CSSFloat::Left ) { + Vector2f bcWorld = bc->convertToWorldSpace( { 0, 0 } ); + // Float children should be at the left edge of the box (not shifted to center) + EXPECT_NEAR( bcWorld.x, boxOrigin.x, 1.f ); + } + } + boxChild = boxChild->getNextNode(); + } + + Engine::destroySingleton(); +} diff --git a/src/tests/unit_tests/utest.hpp b/src/tests/unit_tests/utest.hpp index b5f5e146d..13dd25db8 100644 --- a/src/tests/unit_tests/utest.hpp +++ b/src/tests/unit_tests/utest.hpp @@ -1,18 +1,20 @@ #pragma once #include "utest.h" +#include +#include #include -#include -template std::string vectorToString( const std::vector& vec ) { +template std::string vectorToString( const Range& vec ) { std::ostringstream oss; oss << "["; - bool first = true; - for ( const auto& element : vec ) { - if ( !first ) - oss << ", "; - oss << element; - first = false; + auto it = std::ranges::begin( vec ); + auto end = std::ranges::end( vec ); + if ( it != end ) { + oss << *it; + for ( ++it; it != end; ++it ) { + oss << ", " << *it; + } } oss << "]"; return oss.str();