More WIP, several HTML fixes compatibility.

This commit is contained in:
Martín Lucas Golini
2026-05-24 15:29:00 -03:00
parent c5e39f4c6d
commit fa537eaaaa
11 changed files with 496 additions and 38 deletions

View File

@@ -41,7 +41,12 @@ Current progress:
- The smoke test no longer loads `breeze.css`; HTML defaults now come from automatic HTML base-default injection.
- Legacy `<strike>` is registered as an HTML phrasing element and receives default `line-through` styling, removing a missing-element warning from the fixture.
- Horizontal `auto` margins are recomputed at RichText/block layout read points, and the old Reddit vote arrow is centered inside `.midcol` (`arrow.x=17`, `midcol.x=15`, `arrow.width=15`, `midcol.width=19` in the current smoke run).
- Remaining visible blockers: header/topbar layout is still badly overlapped, vote arrow sprite painting/background positioning still needs verification, the comment form spacing is too large, and the footer/comments vertical spacing still diverges from Chrome.
- Vote arrow sprite CSS now resolves relative to the stylesheet file, preserves negative `background-position`, and is asserted in both a reduced sprite test and the old Reddit smoke test.
- CSS `white-space` now maps browser values such as `normal`, `nowrap`, `pre`, `pre-wrap`, `pre-line`, and `break-spaces` into RichText whitespace-collapse and soft-wrap behavior. `nowrap` suppresses soft wrapping for text and atomic inline boxes, including old Reddit-style inline flat lists.
- Collapsed whitespace-only text nodes between non-inline boxes no longer create a spurious line box in RichText layout.
- HTML `<button>` is created as an HTML rich-text element with browser-like inline-block defaults so it can participate in CSS display/float layout while still rendering child text. This fixes the top-row placement of old Reddit's `#redesign-beta-optin-btn`.
- RichText virtual block breaks no longer split a line that contains only floats, allowing a following BFC to remain beside those floats in reduced cases.
- Remaining visible blockers: the old Reddit subreddit list `.sr-list` still lands at `y=18` and overlaps `#header-bottom-left` despite now being `white-space: nowrap`; the comment form spacing is too large; footer/comments vertical spacing still diverges from Chrome; additional form-control and header sprite details still need visual tightening.
## Reference Layout Invariants
@@ -322,13 +327,14 @@ ASAN_OPTIONS=detect_leaks=0 xvfb-run -a -s "-screen 0 1280x1024x24" bin/unit_tes
### Phase 3: CSS Visual Features
Prioritize sprite backgrounds, URL resolution, background-position, repeat modes, and auto margins. These are the biggest remaining visual gap after layout boxes are placed correctly.
Prioritize sprite backgrounds, URL resolution, background-position, repeat modes, auto margins, and header/topbar CSS behaviors. These are the biggest remaining visual gap after layout boxes are placed correctly.
Exit criteria:
- Vote arrows draw from the sprite and are centered in `.midcol`.
- Vote arrows draw from the sprite and are centered in `.midcol`. (Reduced and old Reddit smoke coverage exists.)
- Header/logo sprite regions render.
- Submit button strips and nubs render close to reference.
- The `#sr-header-area .sr-list` BFC sits in the top row beside the floated redesign button and subreddit dropdown, instead of overlapping `#header-bottom-left`.
### Phase 4: Defaults And Form Controls

View File

@@ -135,6 +135,12 @@ class EE_API RichText : public Drawable {
/** @return The maximum width for wrapping. */
Float getMaxWidth() const { return mMaxWidth; }
/** @brief Enables or disables soft wrapping when max width is set. */
void setLineWrap( bool lineWrap );
/** @return Whether soft wrapping is enabled. */
bool getLineWrap() const { return mLineWrap; }
bool setExternalFloatExclusions( const std::vector<FloatExclusion>& exclusions );
const std::vector<FloatExclusion>& getExternalFloatExclusions() const {
@@ -418,6 +424,7 @@ class EE_API RichText : public Drawable {
bool mNeedsLayoutUpdate{ true };
Float mLineHeight{ 0 };
Float mTextIndent{ 0 };
bool mLineWrap{ true };
};
}} // namespace EE::Graphics

View File

@@ -82,6 +82,7 @@ enum class PropertyId : Uint32 {
FontWeight = String::hash( "font-weight" ),
TextDecoration = String::hash( "text-decoration" ),
Wordwrap = String::hash( "word-wrap" ),
WhiteSpace = String::hash( "white-space" ),
WhiteSpaceCollapse = String::hash( "white-space-collapse" ),
TextStrokeWidth = String::hash( "text-stroke-width" ),
TextStrokeColor = String::hash( "text-stroke-color" ),

View File

@@ -24,6 +24,8 @@ class EE_API UIRichText : public UIHTMLWidget {
static std::string fromWhiteSpaceCollapse( WhiteSpaceCollapse val );
static std::string fromWhiteSpace( WhiteSpaceCollapse collapse, bool lineWrap );
static String collapseInternalWhitespace( const String& s );
static void rebuildRichText( UILayout* container, RichText& richText,
@@ -130,6 +132,12 @@ class EE_API UIRichText : public UIHTMLWidget {
void setWhiteSpaceCollapse( WhiteSpaceCollapse collapse );
bool getLineWrap() const;
void setLineWrap( bool lineWrap );
void applyWhiteSpace( std::string val );
Float getLineHeightPx() const;
UIRichText* setLineHeightEq( const std::string& eq );
@@ -174,6 +182,7 @@ class EE_API UIRichText : public UIHTMLWidget {
mutable Float mTextIndentPxCache{ 0 };
mutable bool mTextIndentPxDirty{ true };
WhiteSpaceCollapse mWhiteSpaceCollapse{ WhiteSpaceCollapse::Collapse };
bool mLineWrap{ true };
TextTransform::Value mTextTransform{ TextTransform::None };
explicit UIRichText( const std::string& tag = "richtext" );

View File

@@ -843,8 +843,8 @@ class RichTextInlineLayouter {
static LayoutResult layoutNoFloats( const std::vector<RichText::InlineItem>& inlineItems,
Float maxLayoutWidth, Float textIndent, Uint32 align,
Float forcedLineHeight,
const FontStyleConfig& defaultStyle ) {
Float forcedLineHeight, const FontStyleConfig& defaultStyle,
bool lineWrap ) {
LayoutResult result;
result.lines.push_back( RichText::RenderParagraph() );
@@ -877,7 +877,7 @@ class RichTextInlineLayouter {
inlineStartSpacing( payload, inlineItems ) );
LineWrapInfoEx wrapInfo = computeTextWraps( payload, fontStyle, maxLayoutWidth,
maxLayoutWidth > 0, curX );
lineWrap && maxLayoutWidth > 0, curX );
for ( size_t i = 0; i < wrapInfo.wraps.size() - 1; ++i ) {
size_t startIdx = wrapInfo.wraps[i];
@@ -934,7 +934,7 @@ class RichTextInlineLayouter {
bool hadLineContentBeforeSpacing = !result.lines.back().spans.empty();
addInlineSpacingToCurrentLine( result, curX, startSpacing );
if ( maxLayoutWidth > 0 &&
if ( lineWrap && maxLayoutWidth > 0 &&
( curX + metrics.size.getWidth() >= maxLayoutWidth ||
curX >= maxLayoutWidth ) &&
curX > 0 && hadLineContentBeforeSpacing ) {
@@ -947,7 +947,7 @@ class RichTextInlineLayouter {
appendAtomicRenderSpan( result.lines.back(), payload, metrics, curX, curCharIdx );
addInlineSpacingToCurrentLine( result, curX, endSpacing );
if ( maxLayoutWidth > 0 && curX >= maxLayoutWidth ) {
if ( lineWrap && maxLayoutWidth > 0 && curX >= maxLayoutWidth ) {
maxWidth = std::max( maxWidth, curX );
result.lines.push_back( RichText::RenderParagraph() );
curX = 0;
@@ -975,11 +975,10 @@ class RichTextInlineLayouter {
return result;
}
static LayoutResult
layoutWithFloats( const std::vector<RichText::InlineItem>& inlineItems, Float maxLayoutWidth,
Float textIndent, Uint32 align, Float forcedLineHeight,
const FontStyleConfig& defaultStyle,
const std::vector<RichText::FloatExclusion>& externalFloatExclusions ) {
static LayoutResult layoutWithFloats(
const std::vector<RichText::InlineItem>& inlineItems, Float maxLayoutWidth,
Float textIndent, Uint32 align, Float forcedLineHeight, const FontStyleConfig& defaultStyle,
const std::vector<RichText::FloatExclusion>& externalFloatExclusions, bool lineWrap ) {
LayoutResult result;
result.lines.push_back( RichText::RenderParagraph() );
@@ -1090,7 +1089,7 @@ class RichTextInlineLayouter {
effW = maxLayoutWidth;
LineWrapInfoEx wrapInfo =
computeTextWraps( payload, fontStyle, effW, effW > 0, curX );
computeTextWraps( payload, fontStyle, effW, lineWrap && effW > 0, curX );
for ( size_t i = 0; i < wrapInfo.wraps.size() - 1; ++i ) {
size_t startIdx = wrapInfo.wraps[i];
@@ -1132,7 +1131,8 @@ class RichTextInlineLayouter {
if ( metrics.isLineBreak ) {
maxWidth = std::max( maxWidth, curX );
if ( !result.lines.back().spans.empty() ) {
if ( !result.lines.back().spans.empty() &&
lineHasInFlowContent( result.lines.back() ) ) {
curY += result.lines.back().height;
result.lines.push_back( RichText::RenderParagraph() );
result.lines.back().y = curY;
@@ -1232,7 +1232,7 @@ class RichTextInlineLayouter {
}
}
if ( !metrics.isBlock && effW > 0 && effW < 1e9f &&
if ( lineWrap && !metrics.isBlock && effW > 0 && effW < 1e9f &&
metrics.size.getWidth() > effW + 0.01f ) {
Float maxBottom = activeFloatBottom( curY );
if ( maxBottom > curY ) {
@@ -1247,7 +1247,7 @@ class RichTextInlineLayouter {
}
}
if ( !metrics.isBlock && effW > 0 && effW < 1e9f &&
if ( lineWrap && !metrics.isBlock && effW > 0 && effW < 1e9f &&
( curX + metrics.size.getWidth() >= effW || curX >= effW ) && curX > 0 &&
hadLineContentBeforeSpacing ) {
maxWidth = std::max( maxWidth, curX );
@@ -1261,7 +1261,7 @@ class RichTextInlineLayouter {
curCharIdx );
addInlineSpacingToCurrentLine( result, curX, endSpacing );
if ( effW > 0 && effW < 1e9f && curX >= effW ) {
if ( lineWrap && effW > 0 && effW < 1e9f && curX >= effW ) {
maxWidth = std::max( maxWidth, curX );
result.lines.push_back( RichText::RenderParagraph() );
curX = 0;
@@ -1706,6 +1706,14 @@ class RichTextInlineLayouter {
line.width += metrics.size.getWidth();
}
static bool lineHasInFlowContent( const RichText::RenderParagraph& line ) {
for ( const auto& span : line.spans ) {
if ( span.floatType == RichText::InlineFloat::None )
return true;
}
return false;
}
static Float horizontalAlignmentOffset( const RichText::RenderParagraph& line,
Float maxLayoutWidth, Uint32 align ) {
if ( maxLayoutWidth <= 0 || align == 0 )
@@ -2036,6 +2044,13 @@ void RichText::setMaxWidth( Float width ) {
}
}
void RichText::setLineWrap( bool lineWrap ) {
if ( mLineWrap != lineWrap ) {
mLineWrap = lineWrap;
invalidateLayout();
}
}
bool RichText::setExternalFloatExclusions( const std::vector<FloatExclusion>& exclusions ) {
if ( mExternalFloatExclusions == exclusions )
return false;
@@ -2067,8 +2082,8 @@ void RichText::updateLayout() {
// ─── Inline layouter fast path: no floats or clears ─────────────
if ( !hasFloats ) {
auto result = RichTextInlineLayouter::layoutNoFloats( mInlineItems, mMaxWidth, mTextIndent,
mAlign, mLineHeight, mDefaultStyle );
auto result = RichTextInlineLayouter::layoutNoFloats(
mInlineItems, mMaxWidth, mTextIndent, mAlign, mLineHeight, mDefaultStyle, mLineWrap );
mLines = std::move( result.lines );
mSize = result.size;
mTotalCharacterCount = result.totalCharacterCount;
@@ -2079,7 +2094,7 @@ void RichText::updateLayout() {
auto result = RichTextInlineLayouter::layoutWithFloats( mInlineItems, mMaxWidth, mTextIndent,
mAlign, mLineHeight, mDefaultStyle,
mExternalFloatExclusions );
mExternalFloatExclusions, mLineWrap );
mLines = std::move( result.lines );
mSize = result.size;
mTotalCharacterCount = result.totalCharacterCount;

View File

@@ -338,6 +338,7 @@ void StyleSheetSpecification::registerDefaultProperties() {
registerProperty( "word-wrap", "" ).setType( PropertyType::Bool );
registerProperty( "white-space", "normal", true ).setType( PropertyType::String );
registerProperty( "white-space-collapse", "collapse", true ).setType( PropertyType::String );
registerProperty( "hint", "" ).setType( PropertyType::String ).addAlias( "placeholder" );

View File

@@ -788,7 +788,7 @@ Vector2f UINodeDrawable::LayerDrawable::calcPosition( std::string positionXEq,
positionXEq = "center";
if ( positionYEq.empty() )
positionXEq = "center";
positionYEq = "center";
auto posX = String::split( positionXEq, ' ' );
auto posY = String::split( positionYEq, ' ' );

View File

@@ -52,6 +52,22 @@ std::string UIRichText::fromWhiteSpaceCollapse( WhiteSpaceCollapse val ) {
}
}
std::string UIRichText::fromWhiteSpace( WhiteSpaceCollapse collapse, bool lineWrap ) {
switch ( collapse ) {
case WhiteSpaceCollapse::Preserve:
return lineWrap ? "pre-wrap" : "pre";
case WhiteSpaceCollapse::PreserveBreaks:
return "pre-line";
case WhiteSpaceCollapse::BreakSpaces:
return "break-spaces";
case WhiteSpaceCollapse::PreserveSpaces:
return lineWrap ? "preserve-spaces" : "preserve nowrap";
case WhiteSpaceCollapse::Collapse:
default:
return lineWrap ? "normal" : "nowrap";
}
}
UIHTMLHtml* UIHTMLHtml::New( const std::string& tag ) {
return eeNew( UIHTMLHtml, ( tag ) );
}
@@ -369,6 +385,9 @@ bool UIRichText::applyProperty( const StyleSheetProperty& attribute ) {
case PropertyId::TextIndent:
setTextIndentEq( attribute.value() );
break;
case PropertyId::WhiteSpace:
applyWhiteSpace( attribute.value() );
break;
case PropertyId::WhiteSpaceCollapse:
setWhiteSpaceCollapse( toWhiteSpaceCollapse( attribute.value() ) );
break;
@@ -424,6 +443,8 @@ std::string UIRichText::getPropertyString( const PropertyDefinition* propertyDef
return mLineHeightEq.empty() ? "normal" : mLineHeightEq;
case PropertyId::TextIndent:
return mTextIndentEq.empty() ? "0" : mTextIndentEq;
case PropertyId::WhiteSpace:
return fromWhiteSpace( mWhiteSpaceCollapse, mLineWrap );
case PropertyId::WhiteSpaceCollapse:
return fromWhiteSpaceCollapse( mWhiteSpaceCollapse );
case PropertyId::TextTransform:
@@ -435,15 +456,13 @@ std::string UIRichText::getPropertyString( const PropertyDefinition* propertyDef
std::vector<PropertyId> UIRichText::getPropertiesImplemented() const {
auto props = UIHTMLWidget::getPropertiesImplemented();
auto local = { PropertyId::FontFamily, PropertyId::FontSize,
PropertyId::FontStyle, PropertyId::Color,
PropertyId::TextShadowColor, PropertyId::TextShadowOffset,
PropertyId::TextStrokeWidth, PropertyId::TextStrokeColor,
PropertyId::TextAlign, PropertyId::SelectionColor,
PropertyId::SelectionBackColor, PropertyId::TextSelection,
PropertyId::TextDecoration, PropertyId::LineHeight,
PropertyId::TextIndent, PropertyId::WhiteSpaceCollapse,
PropertyId::TextTransform };
auto local = {
PropertyId::FontFamily, PropertyId::FontSize, PropertyId::FontStyle,
PropertyId::Color, PropertyId::TextShadowColor, PropertyId::TextShadowOffset,
PropertyId::TextStrokeWidth, PropertyId::TextStrokeColor, PropertyId::TextAlign,
PropertyId::SelectionColor, PropertyId::SelectionBackColor, PropertyId::TextSelection,
PropertyId::TextDecoration, PropertyId::LineHeight, PropertyId::TextIndent,
PropertyId::WhiteSpace, PropertyId::WhiteSpaceCollapse, PropertyId::TextTransform };
props.insert( props.end(), local.begin(), local.end() );
return props;
}
@@ -632,6 +651,42 @@ void UIRichText::setWhiteSpaceCollapse( WhiteSpaceCollapse collapse ) {
}
}
bool UIRichText::getLineWrap() const {
return mLineWrap;
}
void UIRichText::setLineWrap( bool lineWrap ) {
if ( mLineWrap != lineWrap ) {
mLineWrap = lineWrap;
notifyLayoutAttrChange();
notifyLayoutAttrChangeParent();
}
}
void UIRichText::applyWhiteSpace( std::string val ) {
String::toLowerInPlace( val );
String::trimInPlace( val );
if ( val == "normal" ) {
setWhiteSpaceCollapse( WhiteSpaceCollapse::Collapse );
setLineWrap( true );
} else if ( val == "nowrap" ) {
setWhiteSpaceCollapse( WhiteSpaceCollapse::Collapse );
setLineWrap( false );
} else if ( val == "pre" ) {
setWhiteSpaceCollapse( WhiteSpaceCollapse::Preserve );
setLineWrap( false );
} else if ( val == "pre-wrap" ) {
setWhiteSpaceCollapse( WhiteSpaceCollapse::Preserve );
setLineWrap( true );
} else if ( val == "pre-line" ) {
setWhiteSpaceCollapse( WhiteSpaceCollapse::PreserveBreaks );
setLineWrap( true );
} else if ( val == "break-spaces" ) {
setWhiteSpaceCollapse( WhiteSpaceCollapse::BreakSpaces );
setLineWrap( true );
}
}
UIRichText* UIRichText::setLineHeightEq( const std::string& eq ) {
if ( mLineHeightEq != eq ) {
mLineHeightEq = eq;
@@ -1010,6 +1065,7 @@ void UIRichText::rebuildRichText( UILayout* container, RichText& richText, Intri
auto* uiRt = static_cast<UIRichText*>( container );
richText.setLineHeight( uiRt->getLineHeightPx() );
richText.setTextIndent( uiRt->getTextIndentPx() );
richText.setLineWrap( uiRt->getLineWrap() );
}
bool shouldCollapse = container->isType( UI_TYPE_RICHTEXT )
? static_cast<UIRichText*>( container )->getWhiteSpaceCollapse() ==
@@ -1153,6 +1209,12 @@ void UIRichText::rebuildRichText( UILayout* container, RichText& richText, Intri
bool nextIsInline =
next && next->isWidget() && next->asType<UIWidget>()->isInlineDisplay();
if ( shouldCollapse && textNode->isWhitespaceOnly() && !prevIsInline &&
!nextIsInline ) {
textNode->setLayoutCharCount( 0 );
return;
}
// Strip leading space if prev is not inline (block boundary)
if ( !prevIsInline && !text.empty() && text[0] == ' ' )
text = text.substr( 1 );

View File

@@ -119,9 +119,11 @@ input[type="number"] {
hint-color: #767676;
}
button,
input[type="submit"],
input[type="button"],
input[type="reset"] {
display: inline-block;
border-width: 1dp;
border-color: #767676;
background-color: #f0f0f0;
@@ -133,6 +135,7 @@ input[type="reset"] {
text-decoration: none;
}
button:hover,
input[type="submit"]:hover,
input[type="button"]:hover,
input[type="reset"]:hover {
@@ -343,12 +346,7 @@ void UIWidgetCreator::createBaseWidgetList() {
registeredWidget["td"] = [] { return UIHTMLTableCell::New( "td" ); };
registeredWidget["input"] = UIHTMLInput::New;
registeredWidget["textarea"] = UIHTMLTextArea::New;
registeredWidget["button"] = [] {
auto but = UIPushButton::NewWithTag( "button" );
but->setFlags( UI_HTML_ELEMENT );
but->setLayoutSizePolicy( SizePolicy::WrapContent, SizePolicy::WrapContent );
return but;
};
registeredWidget["button"] = [] { return UIRichText::NewWithTag( "button" ); };
registeredWidget["webview"] = UIWebView::New;
sBaseListCreated = true;

View File

@@ -277,6 +277,200 @@ UTEST( UIHTMLFloat, leftFloatOverflowHiddenBlockFormattingContextSitsBesideFloat
Engine::destroySingleton();
}
UTEST( UIHTMLFloat, whitespaceBetweenFloatAndBfcDoesNotPushBfcBelowFloat ) {
init_float_test();
UISceneNode* sceneNode = SceneManager::instance()->getUISceneNode();
sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( R"html(
<body style="margin:0">
<div id="container" style="width:600px">
<div id="left" style="float:left;width:140px;height:18px"></div>
<div id="bfc" style="overflow:hidden;height:18px">top row</div>
</div>
</body>
)html" ) );
sceneNode->updateDirtyLayouts();
auto* left = sceneNode->find<UIWidget>( "left" );
auto* bfc = sceneNode->find<UIWidget>( "bfc" );
ASSERT_TRUE( left != nullptr );
ASSERT_TRUE( bfc != nullptr );
Vector2f leftPos = left->convertToWorldSpace( { 0, 0 } );
Vector2f bfcPos = bfc->convertToWorldSpace( { 0, 0 } );
EXPECT_NEAR( leftPos.y, bfcPos.y, 1.f );
EXPECT_GE( bfcPos.x, leftPos.x + left->getPixelsSize().getWidth() - 1.f );
Engine::destroySingleton();
}
UTEST( UIHTMLFloat, htmlButtonCanFloatBeforeBlockFormattingContext ) {
init_float_test();
UISceneNode* sceneNode = SceneManager::instance()->getUISceneNode();
sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( R"html(
<body style="margin:0">
<div id="container" style="width:600px">
<button id="button" style="float:left;width:140px;height:18px">action</button>
<div id="bfc" style="overflow:hidden;height:18px">top row</div>
</div>
</body>
)html" ) );
sceneNode->updateDirtyLayouts();
auto* button = sceneNode->find<UIWidget>( "button" );
auto* bfc = sceneNode->find<UIWidget>( "bfc" );
ASSERT_TRUE( button != nullptr );
ASSERT_TRUE( bfc != nullptr );
ASSERT_TRUE( button->isType( UI_TYPE_HTML_WIDGET ) );
EXPECT_EQ( button->asType<UIHTMLWidget>()->getCSSFloat(), CSSFloat::Left );
Vector2f buttonPos = button->convertToWorldSpace( { 0, 0 } );
Vector2f bfcPos = bfc->convertToWorldSpace( { 0, 0 } );
EXPECT_NEAR( buttonPos.y, bfcPos.y, 1.f );
EXPECT_GE( bfcPos.x, buttonPos.x + button->getPixelsSize().getWidth() - 1.f );
Engine::destroySingleton();
}
UTEST( UIHTMLFloat, bfcAfterFloatOnlyLineStaysOnSameRow ) {
init_float_test();
UISceneNode* sceneNode = SceneManager::instance()->getUISceneNode();
sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( R"html(
<body style="margin:0">
<div id="container" style="width:600px; line-height:18px">
<button id="button" style="float:left;width:138px;height:18px">action</button>
<div id="drop" style="float:left;width:117px;height:18px">menu</div>
<div id="bfc" style="overflow:hidden;height:18px; white-space:nowrap">top row</div>
</div>
</body>
)html" ) );
sceneNode->updateDirtyLayouts();
auto* button = sceneNode->find<UIWidget>( "button" );
auto* drop = sceneNode->find<UIWidget>( "drop" );
auto* bfc = sceneNode->find<UIWidget>( "bfc" );
ASSERT_TRUE( button != nullptr );
ASSERT_TRUE( drop != nullptr );
ASSERT_TRUE( bfc != nullptr );
Vector2f buttonPos = button->convertToWorldSpace( { 0, 0 } );
Vector2f dropPos = drop->convertToWorldSpace( { 0, 0 } );
Vector2f bfcPos = bfc->convertToWorldSpace( { 0, 0 } );
EXPECT_NEAR( buttonPos.y, bfcPos.y, 1.f );
EXPECT_NEAR( dropPos.y, bfcPos.y, 1.f );
EXPECT_GE( bfcPos.x, dropPos.x + drop->getPixelsSize().getWidth() - 1.f );
Engine::destroySingleton();
}
UTEST( UIHTMLFloat, autoHeightBfcAfterFloatOnlyLineStaysOnSameRow ) {
init_float_test();
UISceneNode* sceneNode = SceneManager::instance()->getUISceneNode();
sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( R"html(
<body style="margin:0">
<div id="container" style="width:600px; line-height:18px">
<button id="button" style="float:left;width:138px;height:18px">action</button>
<div id="drop" style="float:left;width:117px;height:18px">menu</div>
<div id="bfc" style="overflow:hidden; white-space:nowrap">
<span>home</span><span> - popular</span><span> - all</span>
</div>
</div>
</body>
)html" ) );
sceneNode->updateDirtyLayouts();
auto* drop = sceneNode->find<UIWidget>( "drop" );
auto* bfc = sceneNode->find<UIWidget>( "bfc" );
ASSERT_TRUE( drop != nullptr );
ASSERT_TRUE( bfc != nullptr );
Vector2f dropPos = drop->convertToWorldSpace( { 0, 0 } );
Vector2f bfcPos = bfc->convertToWorldSpace( { 0, 0 } );
EXPECT_NEAR( dropPos.y, bfcPos.y, 1.f );
EXPECT_GE( bfcPos.x, dropPos.x + drop->getPixelsSize().getWidth() - 1.f );
Engine::destroySingleton();
}
UTEST( UIHTMLFloat, inlineListBfcAfterFloatOnlyLineStaysOnSameRow ) {
init_float_test();
UISceneNode* sceneNode = SceneManager::instance()->getUISceneNode();
sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( R"html(
<body style="margin:0">
<div id="container" style="width:600px; line-height:18px; white-space:nowrap">
<button id="button" style="float:left;width:138px;height:18px">action</button>
<div id="drop" style="float:left;width:117px;height:18px">menu</div>
<div id="bfc" style="overflow:hidden">
<ul id="list" style="display:inline;list-style:none;margin:0;padding:0">
<li style="display:inline;white-space:nowrap">home</li>
<li style="display:inline;white-space:nowrap"> - popular</li>
<li style="display:inline;white-space:nowrap"> - all</li>
</ul>
</div>
</div>
</body>
)html" ) );
sceneNode->updateDirtyLayouts();
auto* drop = sceneNode->find<UIWidget>( "drop" );
auto* bfc = sceneNode->find<UIWidget>( "bfc" );
ASSERT_TRUE( drop != nullptr );
ASSERT_TRUE( bfc != nullptr );
Vector2f dropPos = drop->convertToWorldSpace( { 0, 0 } );
Vector2f bfcPos = bfc->convertToWorldSpace( { 0, 0 } );
EXPECT_NEAR( dropPos.y, bfcPos.y, 1.f );
EXPECT_GE( bfcPos.x, dropPos.x + drop->getPixelsSize().getWidth() - 1.f );
Engine::destroySingleton();
}
UTEST( UIHTMLFloat, absoluteInlineListBfcAfterFloatOnlyLineStaysOnSameRow ) {
init_float_test();
UISceneNode* sceneNode = SceneManager::instance()->getUISceneNode();
sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( R"html(
<body style="margin:0">
<div id="header" style="position:relative;width:600px;height:18px;line-height:18px;white-space:nowrap">
<div id="clip" style="position:absolute;left:0;right:0">
<button id="button" style="float:left;width:138px;height:18px">action</button>
<div id="drop" style="float:left;width:117px;height:18px">menu</div>
<div id="bfc" style="overflow:hidden">
<ul id="list" style="display:inline;list-style:none;margin:0;padding:0">
<li style="display:inline;white-space:nowrap">home</li>
<li style="display:inline;white-space:nowrap"> - popular</li>
<li style="display:inline;white-space:nowrap"> - all</li>
</ul>
</div>
</div>
</div>
</body>
)html" ) );
sceneNode->updateDirtyLayouts();
auto* drop = sceneNode->find<UIWidget>( "drop" );
auto* bfc = sceneNode->find<UIWidget>( "bfc" );
ASSERT_TRUE( drop != nullptr );
ASSERT_TRUE( bfc != nullptr );
Vector2f dropPos = drop->convertToWorldSpace( { 0, 0 } );
Vector2f bfcPos = bfc->convertToWorldSpace( { 0, 0 } );
EXPECT_NEAR( dropPos.y, bfcPos.y, 1.f );
EXPECT_GE( bfcPos.x, dropPos.x + drop->getPixelsSize().getWidth() - 1.f );
Engine::destroySingleton();
}
UTEST( UIHTMLFloat, autoHorizontalMarginsCenterBlockInsideFloat ) {
init_float_test();
UISceneNode* sceneNode = SceneManager::instance()->getUISceneNode();

View File

@@ -212,12 +212,24 @@ UTEST( UIHTML, redditOldThreadWebViewSmoke ) {
auto midcol = sceneNode->getRoot()->findByClass( "midcol" );
auto entry = sceneNode->getRoot()->findByClass( "entry" );
auto arrow = sceneNode->getRoot()->findByClass( "arrow" );
auto srHeader = sceneNode->getRoot()->find( "sr-header-area" );
auto redesignButton = sceneNode->getRoot()->find( "redesign-beta-optin-btn" );
auto srDrop = sceneNode->getRoot()->querySelector( "#sr-header-area .dropdown.srdrop" );
auto srList = sceneNode->getRoot()->querySelector( "#sr-header-area .sr-list" );
auto headerBottomLeft = sceneNode->getRoot()->find( "header-bottom-left" );
auto dropChoices = sceneNode->getRoot()->querySelector( ".drop-choices.srdrop" );
ASSERT_TRUE( side != nullptr );
ASSERT_TRUE( siteTable != nullptr );
ASSERT_TRUE( midcol != nullptr );
ASSERT_TRUE( entry != nullptr );
ASSERT_TRUE( arrow != nullptr );
ASSERT_TRUE( srHeader != nullptr );
ASSERT_TRUE( redesignButton != nullptr );
ASSERT_TRUE( srDrop != nullptr );
ASSERT_TRUE( srList != nullptr );
ASSERT_TRUE( headerBottomLeft != nullptr );
ASSERT_TRUE( dropChoices != nullptr );
UIWidget* content =
siteTable->getParent()->isWidget() ? siteTable->getParent()->asType<UIWidget>() : nullptr;
@@ -228,6 +240,14 @@ UTEST( UIHTML, redditOldThreadWebViewSmoke ) {
Vector2f midcolPos = midcol->asType<UIWidget>()->convertToWorldSpace( { 0, 0 } );
Vector2f entryPos = entry->asType<UIWidget>()->convertToWorldSpace( { 0, 0 } );
Vector2f arrowPos = arrow->asType<UIWidget>()->convertToWorldSpace( { 0, 0 } );
Vector2f srHeaderPos = srHeader->asType<UIWidget>()->convertToWorldSpace( { 0, 0 } );
Vector2f redesignButtonPos =
redesignButton->asType<UIWidget>()->convertToWorldSpace( { 0, 0 } );
Vector2f srDropPos = srDrop->asType<UIWidget>()->convertToWorldSpace( { 0, 0 } );
Vector2f srListPos = srList->asType<UIWidget>()->convertToWorldSpace( { 0, 0 } );
Vector2f headerBottomLeftPos =
headerBottomLeft->asType<UIWidget>()->convertToWorldSpace( { 0, 0 } );
Vector2f dropChoicesPos = dropChoices->asType<UIWidget>()->convertToWorldSpace( { 0, 0 } );
std::cerr << "old reddit rects: "
<< "side=(" << sidePos.x << "," << sidePos.y << " "
@@ -244,7 +264,31 @@ UTEST( UIHTML, redditOldThreadWebViewSmoke ) {
<< entry->asType<UIWidget>()->getPixelsSize().getHeight() << ") "
<< "arrow=(" << arrowPos.x << "," << arrowPos.y << " "
<< arrow->asType<UIWidget>()->getPixelsSize().getWidth() << "x"
<< arrow->asType<UIWidget>()->getPixelsSize().getHeight() << ")" << std::endl;
<< arrow->asType<UIWidget>()->getPixelsSize().getHeight() << ") "
<< "srHeader=(" << srHeaderPos.x << "," << srHeaderPos.y << " "
<< srHeader->asType<UIWidget>()->getPixelsSize().getWidth() << "x"
<< srHeader->asType<UIWidget>()->getPixelsSize().getHeight() << ") "
<< "redesignButton=(" << redesignButtonPos.x << "," << redesignButtonPos.y << " "
<< redesignButton->asType<UIWidget>()->getPixelsSize().getWidth() << "x"
<< redesignButton->asType<UIWidget>()->getPixelsSize().getHeight() << ") "
<< "srDrop=(" << srDropPos.x << "," << srDropPos.y << " "
<< srDrop->asType<UIWidget>()->getPixelsSize().getWidth() << "x"
<< srDrop->asType<UIWidget>()->getPixelsSize().getHeight() << " float="
<< CSSFloatHelper::toString( srDrop->asType<UIHTMLWidget>()->getCSSFloat() ) << ") "
<< "srList=(" << srListPos.x << "," << srListPos.y << " "
<< srList->asType<UIWidget>()->getPixelsSize().getWidth() << "x"
<< srList->asType<UIWidget>()->getPixelsSize().getHeight()
<< " lines=" << srList->asType<UIRichText>()->getRichTextPtr()->getLines().size()
<< " wrap=" << srList->asType<UIRichText>()->getLineWrap() << ") "
<< "headerBottomLeft=(" << headerBottomLeftPos.x << "," << headerBottomLeftPos.y
<< " " << headerBottomLeft->asType<UIWidget>()->getPixelsSize().getWidth() << "x"
<< headerBottomLeft->asType<UIWidget>()->getPixelsSize().getHeight() << ") "
<< "dropChoices=(" << dropChoicesPos.x << "," << dropChoicesPos.y << " "
<< dropChoices->asType<UIWidget>()->getPixelsSize().getWidth() << "x"
<< dropChoices->asType<UIWidget>()->getPixelsSize().getHeight()
<< " visible=" << dropChoices->asType<UIWidget>()->isVisible() << " display="
<< CSSDisplayHelper::toString( dropChoices->asType<UIHTMLWidget>()->getDisplay() )
<< ")" << std::endl;
const Float midcolCenter =
midcolPos.x + midcol->asType<UIWidget>()->getPixelsSize().getWidth() / 2.f;
@@ -252,6 +296,18 @@ UTEST( UIHTML, redditOldThreadWebViewSmoke ) {
arrowPos.x + arrow->asType<UIWidget>()->getPixelsSize().getWidth() / 2.f;
EXPECT_NEAR( midcolCenter, arrowCenter, 1.f );
auto* arrowBackground = arrow->asType<UIWidget>()->getBackground();
ASSERT_TRUE( arrowBackground != nullptr );
auto* arrowBackgroundLayer = arrowBackground->getLayer( 0 );
ASSERT_TRUE( arrowBackgroundLayer != nullptr );
ASSERT_TRUE( arrowBackgroundLayer->getDrawable() != nullptr );
EXPECT_EQ( arrowBackgroundLayer->getDrawable()->getPixelsSize().getWidth(), 140 );
EXPECT_EQ( arrowBackgroundLayer->getDrawable()->getPixelsSize().getHeight(), 1751 );
EXPECT_STDSTREQ( arrowBackgroundLayer->getPositionX(), "-42px" );
EXPECT_STDSTREQ( arrowBackgroundLayer->getPositionY(), "-1678px" );
EXPECT_NEAR( arrowBackgroundLayer->getOffset().x, -42.f, 0.1f );
EXPECT_NEAR( arrowBackgroundLayer->getOffset().y, -1678.f, 0.1f );
if ( !FileSystem::fileExists( "output" ) )
FileSystem::makeDir( "output" );
win->getFrontBufferImage().saveToFile( "output/eepp-reddit-old-thread-current.webp",
@@ -275,6 +331,70 @@ UTEST( UIHTML, StrikeElementUsesDefaultLineThrough ) {
Engine::destroySingleton();
}
UTEST( UIHTML, WhiteSpaceNowrapDisablesSoftWrap ) {
init_ui_test();
UISceneNode* sceneNode = SceneManager::instance()->getUISceneNode();
const std::string text = "alpha beta gamma delta epsilon zeta eta theta iota kappa lambda mu";
sceneNode->loadLayoutFromString(
HTMLFormatter::HTMLtoXML( String::format( R"html(
<body>
<div id="wrap" style="width: 120px; font-size: 16px">%s</div>
<div id="nowrap" style="width: 120px; font-size: 16px; white-space: nowrap">
%s
</div>
</body>
)html",
text.c_str(), text.c_str() ) ) );
SceneManager::instance()->update();
auto* wrap = sceneNode->getRoot()->find( "wrap" )->asType<UIRichText>();
auto* nowrap = sceneNode->getRoot()->find( "nowrap" )->asType<UIRichText>();
ASSERT_TRUE( wrap != nullptr );
ASSERT_TRUE( nowrap != nullptr );
EXPECT_GT( wrap->getRichTextPtr()->getLines().size(), (size_t)1 );
EXPECT_EQ( nowrap->getRichTextPtr()->getLines().size(), (size_t)1 );
EXPECT_FALSE( nowrap->getLineWrap() );
Engine::destroySingleton();
}
UTEST( UIHTML, WhiteSpaceNowrapKeepsInlineListOnOneLine ) {
init_ui_test();
UISceneNode* sceneNode = SceneManager::instance()->getUISceneNode();
sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( R"html(
<body>
<div id="bar" style="width: 120px; white-space: nowrap; overflow: hidden">
<ul id="list" style="display: inline; list-style: none; margin: 0; padding: 0">
<li style="display: inline; white-space: nowrap">alpha</li>
<li style="display: inline; white-space: nowrap">beta</li>
<li style="display: inline; white-space: nowrap">gamma</li>
<li style="display: inline; white-space: nowrap">delta</li>
<li style="display: inline; white-space: nowrap">epsilon</li>
<li style="display: inline; white-space: nowrap">zeta</li>
<li style="display: inline; white-space: nowrap">eta</li>
<li style="display: inline; white-space: nowrap">theta</li>
</ul>
</div>
</body>
)html" ) );
SceneManager::instance()->update();
auto* bar = sceneNode->getRoot()->find( "bar" )->asType<UIRichText>();
auto* list = sceneNode->getRoot()->find( "list" )->asType<UIRichText>();
ASSERT_TRUE( bar != nullptr );
ASSERT_TRUE( list != nullptr );
EXPECT_EQ( bar->getRichTextPtr()->getLines().size(), (size_t)1 );
EXPECT_FALSE( bar->getLineWrap() );
EXPECT_FALSE( list->getLineWrap() );
Engine::destroySingleton();
}
UTEST( UIRichText, anchorMargins ) {
auto win = Engine::instance()->createWindow(
WindowSettings( 800, 600, "Anchor Margins Test", WindowStyle::Default,
@@ -2299,6 +2419,51 @@ UTEST( UIBackground, imageAtlasPositioningPixelDensity2 ) {
EE::Graphics::PixelDensity::setPixelDensity( 1.0f );
}
UTEST( UIBackground, cssFileRelativeSpriteUrlAndNegativePosition ) {
init_ui_test();
UISceneNode* sceneNode = SceneManager::instance()->getUISceneNode();
sceneNode->setURI( "file://" + Sys::getProcessPath() + "assets/html/" );
sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( R"html(
<body style="margin:0">
<div id="sprite" class="sprite"></div>
</body>
)html" ) );
sceneNode->combineStyleSheet(
R"css(
.sprite {
width: 15px;
height: 14px;
background-image: url(sprite-reddit.13AvZYXRW_4.png);
background-position: -42px -1678px;
background-repeat: no-repeat;
}
)css",
true, String::hash( "reddit-sprite-test" ),
URI( "file://" + Sys::getProcessPath() +
"assets/html/reddit_old_thread_files/reddit-sprite-test.css" ) );
sceneNode->updateDirtyLayouts();
auto* sprite = sceneNode->find<UIWidget>( "sprite" );
ASSERT_TRUE( sprite != nullptr );
auto* background = sprite->getBackground();
ASSERT_TRUE( background != nullptr );
auto* layer = background->getLayer( 0 );
ASSERT_TRUE( layer != nullptr );
ASSERT_TRUE( layer->getDrawable() != nullptr );
EXPECT_EQ( layer->getDrawable()->getPixelsSize().getWidth(), 140 );
EXPECT_EQ( layer->getDrawable()->getPixelsSize().getHeight(), 1751 );
EXPECT_STDSTREQ( layer->getPositionX(), "-42px" );
EXPECT_STDSTREQ( layer->getPositionY(), "-1678px" );
EXPECT_NEAR( layer->getOffset().x, -42.f, 0.1f );
EXPECT_NEAR( layer->getOffset().y, -1678.f, 0.1f );
EXPECT_EQ( layer->getRepeatX(), UINodeDrawable::RepeatX::NoRepeat );
EXPECT_EQ( layer->getRepeatY(), UINodeDrawable::RepeatY::NoRepeat );
Engine::destroySingleton();
}
UTEST( UIBackground, InlineBlockImageSpans ) {
auto win = Engine::instance()->createWindow(
WindowSettings( 1024, 653, "inline-block image spans", WindowStyle::Default,