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 :::...
+
+
+
+
+
+
+
+
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();