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 34d2d0c8a..9439bc1bb 100644 Binary files a/bin/unit_tests/assets/html/eepp-ui-border-rendering-2.webp and b/bin/unit_tests/assets/html/eepp-ui-border-rendering-2.webp differ 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 e28633cf4..19588b25a 100644 Binary files a/bin/unit_tests/assets/html/eepp-ui-border-rendering.webp and b/bin/unit_tests/assets/html/eepp-ui-border-rendering.webp differ 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();