Add float and clear support.

Fixes in Base64 implementation.
Fixes in remote image loading.
This commit is contained in:
Martín Lucas Golini
2026-05-02 01:12:13 -03:00
parent d54fd398f6
commit 307d9249ac
25 changed files with 1391 additions and 109 deletions

View File

@@ -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

View File

@@ -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 ) ) );

View File

@@ -306,8 +306,9 @@ void RichText::addDrawable( std::shared_ptr<Drawable> 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<CustomBlock>( &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<SpanBlock>( &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<Text> renderSpanText = std::make_shared<Text>();
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<std::shared_ptr<Drawable>>( &block ) ) {
auto& drawable = *pDrawable;
blockSize = drawable ? drawable->getPixelsSize() : Sizef();
} else if ( auto pSize = std::get_if<CustomBlock>( &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<SpanBlock>( &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<Rectf> leftFloats;
std::vector<Rectf> 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<SpanBlock>( &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<std::shared_ptr<Drawable>>( &block ) ) {
auto& drawable = *pDrawable;
blockSize = drawable ? drawable->getPixelsSize() : Sizef();
} else if ( auto pSize = std::get_if<CustomBlock>( &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<CustomBlock>( &span.block ) ) {
if ( pSize->floatType != UI::CSSFloat::None )
isFloat = true;
}
if ( auto pText = std::get_if<SpanBlock>( &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;
}

View File

@@ -2309,7 +2309,7 @@ Uint32 Text::getNumLines() {
return mString.countChar( '\n' ) + 1;
}
const std::vector<Float>& Text::getLinesWidth() {
const SmallVector<Float, 4>& Text::getLinesWidth() {
cacheWidth();
return mLinesWidth;

View File

@@ -538,8 +538,8 @@ TextLayout::Cache TextLayout::layout( const String& string, Font* font, const Ui
keepIndentation, initialXOffset );
}
std::vector<Float> TextLayout::getLinesWidth() const {
std::vector<Float> lw;
SmallVector<Float, 4> TextLayout::getLinesWidth() const {
SmallVector<Float, 4> 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<Float> wrapsWidth = std::move( sp.wrapInfo.wrapsWidth );
auto wrapsWidth = std::move( sp.wrapInfo.wrapsWidth );
sp.wrapInfo.wrapsWidth.clear();
if ( keepIndentation && shapedGlyphCount ) {

View File

@@ -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

View File

@@ -7,6 +7,7 @@
#include <eepp/graphics/triangledrawable.hpp>
#include <eepp/scene/scenemanager.hpp>
#include <eepp/system/log.hpp>
#include <eepp/system/luapattern.hpp>
#include <eepp/ui/css/drawableimageparser.hpp>
#include <eepp/ui/uiiconthememanager.hpp>
#include <eepp/ui/uinode.hpp>
@@ -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&,

View File

@@ -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 ) );

View File

@@ -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<PropertyId> 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;

View File

@@ -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<UIHTMLWidget>()->getCSSFloat();
clearType = widget->asType<UIHTMLWidget>()->getCSSClear();
}
richText.addCustomSize( Sizef( w + margin.Left + margin.Right,
size.getHeight() + margin.Top + margin.Bottom ),
isBlock );
isBlock, floatType, clearType );
}
};

View File

@@ -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<PropertyId> 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;
}

View File

@@ -0,0 +1,607 @@
#include "utest.h"
#include <eepp/graphics/fontfamily.hpp>
#include <eepp/graphics/fonttruetype.hpp>
#include <eepp/scene/scenemanager.hpp>
#include <eepp/system/filesystem.hpp>
#include <eepp/ui/css/stylesheetparser.hpp>
#include <eepp/ui/tools/htmlformatter.hpp>
#include <eepp/ui/uihtmlwidget.hpp>
#include <eepp/ui/uirichtext.hpp>
#include <eepp/ui/uiscenenode.hpp>
#include <eepp/ui/uithememanager.hpp>
#include <eepp/ui/uitheme.hpp>
#include <eepp/window/engine.hpp>
#include <eepp/window/window.hpp>
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<UIWidget>( "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<UIWidget>();
if ( w->isType( UI_TYPE_HTML_WIDGET ) &&
w->asType<UIHTMLWidget>()->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<UIWidget>();
if ( bc->isType( UI_TYPE_HTML_WIDGET ) &&
bc->asType<UIHTMLWidget>()->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();
}

View File

@@ -1,18 +1,20 @@
#pragma once
#include "utest.h"
#include <eepp/core/small_vector.hpp>
#include <ranges>
#include <sstream>
#include <vector>
template <typename T> std::string vectorToString( const std::vector<T>& vec ) {
template <std::ranges::input_range Range> 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();