diff --git a/.agent/plans/css_block_semantics_full_compliance_plan.md b/.agent/plans/css_block_semantics_full_compliance_plan.md new file mode 100644 index 000000000..e649853ff --- /dev/null +++ b/.agent/plans/css_block_semantics_full_compliance_plan.md @@ -0,0 +1,126 @@ +# CSS Block Semantics — Full Compliance Plan + +This plan addresses the remaining pragmatic deviations between the RichText-based layout engine and the CSS 2.1 block formatting context model. The goal is a fully compliant web engine with no special-case "richtext mode" vs. "HTML mode" distinction. + +**AGENT DIRECTIVE (CRITICAL):** Compile and run the unit tests after EVERY step. Take a git stash snapshot (`git stash push -m "Step X.Y passed"`) upon passing a step. Do NOT proceed if any test regresses. + +--- + +## Current Deviation Summary + +| # | Deviation | Impact | +|---|-----------|--------| +| D1 | `isBlock && flowX > 0` allows blocks to sit beside floats when no inline content preceded them. CSS spec: block always starts on a new line (§9.4.1). | Blocks float-side-by-side in float-aware path instead of stacking vertically. | +| D2 | RichText `CustomBlock` with `isBlock` piggybacks line-break behavior onto a text-flow engine. A block formatting context should stack child boxes vertically, not inline them with `isBlock` flags. | Semantic confusion: the RichText engine conflates text flow and block layout. | +| D3 | Inline-block spans are decomposed into RichText text segments that wrap line-by-line, rather than being treated as a single opaque box. | The "solid box" semantics of inline-block are partially emulated with `atomicMaxX` / forced line breaks, but edge cases remain. | +| D4 | `` widgets and `
` containers are both `UIRichText` with `CSSDisplay::Block`, yet `` has historically flowed children inline. The tag-based distinction was removed; now both use CSS display semantics, which is correct. | No remaining deviation — this is resolved in the current PR. | + +## Phase 1: Isolate RichText from Block Layout + +The RichText engine (`Graphics::RichText`) should handle **text formatting only** — word wrapping, line breaking, glyph placement. It should NOT decide whether an element breaks to a new line. That decision belongs to the Block Formatting Context established by the `BlockLayouter`. + +### Step 1.1: Remove `isBlock` from `RichText::CustomBlock` and `RichText::addCustomSize` + +- `CustomBlock` drops the `isBlock` field. +- `addCustomSize` drops the `isBlock` parameter. +- `RichText::updateLayout()` removes all `isBlock`-gated line-break logic (both non-float and float-aware paths). Every `CustomBlock` flows as an inline-level atomic box. +- All `fillParent` / width-override logic stays in `UIRichText::rebuildRichText`. +- **Validation:** Compile. Many tests will fail — this is expected. Continue. + +### Step 1.2: Move block-level line breaking into `BlockLayouter` + +- `BlockLayouter::updateLayout()` now builds the RichText content in **per-child layers** rather than delegating everything to a single `rebuildRichText` call. +- For each child widget: + - If the child is mergeable (text span, inline), it is added to the current RichText via `UIRichText::rebuildRichText`. + - If the child is a block-level element (display != Inline and != InlineBlock), a **new line** is forced before the child. This is done by inserting a `\n` into the RichText or by finalizing the current line and starting a fresh `RenderParagraph`. +- The container's own `positionRichTextChildren` is called once after ALL children are placed. +- **Validation:** All tests pass. (Snapshot) + +### Step 1.3: Remove the `flowX` workaround + +- With block-line-breaking moved into `BlockLayouter`, the `flowX` / `curX` saving logic in the float-aware path is no longer needed. Blocks always start on a new line (per CSS). +- Remove the `flowX` variable and associated logic from `RichText::updateLayout()` float-aware path. +- Remove the "block overflow below floats" block (it's superseded by the block-layouter approach). +- **Validation:** `UIHTML.blockFlow`, `UIHTML.blockFlowFloat`, all `UIHTMLFloat.*` tests pass. (Snapshot) + +## Phase 2: Full Block Formatting Context + +### Step 2.1: Block children interact with floats + +- When `BlockLayouter` places a block child and there are active floats (from the RichText engine at the current Y), the block child's margin box must respect the float constraints: + - If the block fits in the available width (between left-floats and right-floats), it stays on the same line but its width is narrowed. + - If the block does NOT fit, it moves to the next "float-free" Y (below all active floats) — effectively an implicit clear. +- Implement this in `BlockLayouter::updateLayout()` by querying `RichText` for the current float state. +- The existing float overflow logic in `RichText` (for float-on-float overflow) stays in place. The block-vs-float overflow is now handled in `BlockLayouter`. +- **Validation:** `UIHTMLFloat.floatWrapsContentBelowWhenTooWide` and all float tests pass. (Snapshot) + +### Step 2.2: `positionRichTextChildren` handles block-level Y offsets + +- Currently, block and inline children are positioned by the same `positionRichTextChildren` loop, which walks RichText lines and assigns positions. +- With the new approach, block children are placed at the start of a new RichText line (their own line). Their Y position is determined by the line they occupy. +- `BlockLayouter` may need to track which children are block vs. inline so it can query the correct line Y. +- **Validation:** All block-positioning tests pass (`UITextNode_BlockLayouter.*`, `UIHTML.blockFlow`, `UIRichText.MarginsTest`). (Snapshot) + +## Phase 3: Inline-Block Box Semantics + +### Step 3.1: Treat inline-block as an opaque `CustomBlock` + +- In `UIRichText::rebuildRichText`, when a child span has `isInlineBlock() == true`, do NOT flatten its text into the parent RichText. +- Instead, render the inline-block's content into its OWN `RichText` instance (via its own `BlockLayouter`), producing a single `Sizef` representing the box. +- Add this box to the parent RichText via `addCustomSize` (with `floatType = None`, `clearType = None`). No `isBlock` flag needed. +- This makes the inline-block a single opaque rectangle in the parent's inline flow — exactly matching CSS. +- Remove the `atomicMaxX` tracking and multiline forced-break logic from `RichText::updateLayout()` (both paths). +- **Validation:** `UIHTML.InlineBlockBrowserTest`, `UIHTML.InlineBlock*` tests pass. (Snapshot) + +### Step 3.2: Inline-block height and baseline alignment + +- CSS inline-blocks align to the parent's baseline. The `RichText` line layout must handle the inline-block's height correctly (via `maxAscent` and `lineHeight` in `RenderParagraph`). +- If the inline-block contains text, its own RichText produces a height. This height must be passed to the parent's `addCustomSize` as the box height. +- Baseline of the inline-block = baseline of its last line of text (or bottom if no text). +- **Validation:** Inline-block vertical alignment tests pass. (Snapshot) + +## Phase 4: Cleanup and Regression + +### Step 4.1: Remove dead code + +- Remove `isBlock`-related fields from `SpanBlock` and `CustomBlock` structs. +- Remove `isBlock` parameter from `RichText::addSpan` (line-height variant), `RichText::addCustomSize`. +- Remove `atomicMaxX` tracking from both layout paths. +- Remove `flowX` logic from the float-aware path. +- Remove the "block overflow below floats" special case from the float-aware path. +- **Validation:** Full test suite (280 tests). Must all pass. (Snapshot) + +### Step 4.2: Add spec-compliance regression tests + +- Write tests for edge cases identified during migration: + - Block after float with no inline content: block must start on a new line (D1 resolution). + - Block with explicit width after float: block starts below float if it doesn't fit. + - Float → Block → Inline sequence: block goes below float, inline flows beside block. + - Inline-block beside float: inline-block box flows beside float, not decomposed. + - Nested inline-blocks: outer inline-block contains inner inline-block. +- **Validation:** All new tests pass. (Snapshot) + +--- + +## Migration Order (Dependency Graph) + +``` +Phase 1 (isolate RichText) + ├─ 1.1 Remove isBlock from RichText + ├─ 1.2 Move line breaking to BlockLayouter + └─ 1.3 Remove flowX workaround + ↓ +Phase 2 (block formatting context) + ├─ 2.1 Block-float interaction in BlockLayouter + └─ 2.2 positionRichTextChildren block Y offsets + ↓ +Phase 3 (inline-block box semantics) + ├─ 3.1 Inline-block as opaque CustomBlock + └─ 3.2 Baseline alignment + ↓ +Phase 4 (cleanup) + ├─ 4.1 Remove dead code + └─ 4.2 Spec-compliance regression tests +``` + +Each phase is a self-contained checkpoint. No phase should leave the test suite in a broken state. diff --git a/bin/unit_tests/assets/html/block_flow.html b/bin/unit_tests/assets/html/block_flow.html new file mode 100644 index 000000000..aa04c56c4 --- /dev/null +++ b/bin/unit_tests/assets/html/block_flow.html @@ -0,0 +1,87 @@ + + + + Block Layout + + + +
+
+
English
+
    +
  • Hello
  • +
+
+
+
Chinese
+
    +
  • Nǐ hǎo
  • +
+
+
+
Japanese
+
    +
  • Konnichiwa
  • +
+
+
+
Arabic
+
    +
  • Marhaban
  • +
+
+
+
Hebrew
+
    +
  • Shalom
  • +
+
+
+
Russian
+
    +
  • Privet
  • +
+
+
+ + diff --git a/bin/unit_tests/assets/html/block_flow_float_left.html b/bin/unit_tests/assets/html/block_flow_float_left.html new file mode 100644 index 000000000..fa2c88afa --- /dev/null +++ b/bin/unit_tests/assets/html/block_flow_float_left.html @@ -0,0 +1,88 @@ + + + + Block Layout + + + +
+
+
English
+
    +
  • Hello
  • +
+
+
+
Chinese
+
    +
  • Nǐ hǎo
  • +
+
+
+
Japanese
+
    +
  • Konnichiwa
  • +
+
+
+
Arabic
+
    +
  • Marhaban
  • +
+
+
+
Hebrew
+
    +
  • Shalom
  • +
+
+
+
Russian
+
    +
  • Privet
  • +
+
+
+ + diff --git a/bin/unit_tests/assets/html/eepp-ui-background-atlas-pd2.webp b/bin/unit_tests/assets/html/eepp-ui-background-atlas-pd2.webp index a34bc3f61..70052132e 100644 Binary files a/bin/unit_tests/assets/html/eepp-ui-background-atlas-pd2.webp and b/bin/unit_tests/assets/html/eepp-ui-background-atlas-pd2.webp differ diff --git a/bin/unit_tests/assets/html/is_inline_block.html b/bin/unit_tests/assets/html/is_inline_block.html new file mode 100644 index 000000000..ba9ccc0e5 --- /dev/null +++ b/bin/unit_tests/assets/html/is_inline_block.html @@ -0,0 +1,26 @@ + + + + + + +
+ Here is some normal starting text. + This is the target inline-block element. If the container gets too narrow, this solid block drops to the next line, and its internal text will wrap, making the block taller without breaking. + And here is the text that comes immediately after. It gets pushed down correctly. +
+ + diff --git a/src/eepp/graphics/richtext.cpp b/src/eepp/graphics/richtext.cpp index a7b7fa348..85725ec64 100644 --- a/src/eepp/graphics/richtext.cpp +++ b/src/eepp/graphics/richtext.cpp @@ -537,6 +537,7 @@ void RichText::updateLayout() { wrapInfo.wraps.push_back( span->getString().size() ); // Emit a RenderSpan for each segment, wrapping to new lines as needed. + Float atomicMaxX = 0; for ( size_t i = 0; i < wrapInfo.wraps.size() - 1; ++i ) { size_t startIdx = wrapInfo.wraps[i]; size_t endIdx = wrapInfo.wraps[i + 1]; @@ -573,6 +574,8 @@ void RichText::updateLayout() { curX += spanWidth; currentLine.width += spanWidth; + if ( pText->isAtomic ) + atomicMaxX = std::max( atomicMaxX, curX ); } // After the last segment, add trailing margin and check if the @@ -581,6 +584,8 @@ void RichText::updateLayout() { Float extraRight = pText->margin.Right + pText->padding.Right; curX += extraRight; mLines.back().width += extraRight; + if ( pText->isAtomic ) + atomicMaxX = std::max( atomicMaxX, curX ); if ( !isNewline && mMaxWidth > 0 && curX > mMaxWidth ) { maxWidth = std::max( maxWidth, curX ); mLines.push_back( RenderParagraph() ); @@ -604,6 +609,22 @@ void RichText::updateLayout() { curX = 0; } } + + // Atomic (inline-block) spans reserve the width of their widest line + // so subsequent content does not flow beside a shorter last line. + if ( pText->isAtomic && atomicMaxX > curX ) { + curX = atomicMaxX; + if ( !mLines.empty() ) + mLines.back().width = std::max( mLines.back().width, curX ); + } + + // If the inline-block spanned multiple lines, force a new line + // so trailing content starts below the entire block. + if ( pText->isAtomic && wrapInfo.wraps.size() > 2 && curX > 0 ) { + maxWidth = std::max( maxWidth, curX ); + mLines.push_back( RenderParagraph() ); + curX = 0; + } } else { // Drawable or CustomBlock (non-float). Sizef blockSize; @@ -831,6 +852,7 @@ void RichText::updateLayout() { wrapInfo.wraps.back() != (Float)span->getString().size() ) wrapInfo.wraps.push_back( span->getString().size() ); + Float atomicMaxX = 0; for ( size_t i = 0; i < wrapInfo.wraps.size() - 1; ++i ) { size_t startIdx = wrapInfo.wraps[i]; size_t endIdx = wrapInfo.wraps[i + 1]; @@ -866,6 +888,8 @@ void RichText::updateLayout() { curX += spanWidth; currentLine.width += spanWidth; + if ( pText->isAtomic ) + atomicMaxX = std::max( atomicMaxX, curX ); } // After the last segment, add trailing margin and check if the @@ -874,6 +898,8 @@ void RichText::updateLayout() { Float extraRight = pText->margin.Right + pText->padding.Right; curX += extraRight; mLines.back().width += extraRight; + if ( pText->isAtomic ) + atomicMaxX = std::max( atomicMaxX, curX ); if ( effW > 0 && effW < 1e9f && curX > effW ) { maxWidth = std::max( maxWidth, curX ); mLines.push_back( RenderParagraph() ); @@ -897,6 +923,21 @@ void RichText::updateLayout() { curX = 0; } } + + // Atomic (inline-block) spans reserve the width of their widest line. + if ( pText->isAtomic && atomicMaxX > curX ) { + curX = atomicMaxX; + if ( !mLines.empty() ) + mLines.back().width = std::max( mLines.back().width, curX ); + } + + // If the inline-block spanned multiple lines, force a new line + // so trailing content starts below the entire block. + if ( pText->isAtomic && wrapInfo.wraps.size() > 2 && curX > 0 ) { + maxWidth = std::max( maxWidth, curX ); + mLines.push_back( RenderParagraph() ); + curX = 0; + } } else { // ── Drawable or CustomBlock ──────────────────────────── Sizef blockSize; @@ -934,9 +975,41 @@ void RichText::updateLayout() { Float posX; if ( floatType == UI::CSSFloat::Left ) { posX = le; + Float availW = floatRightEdge( curY ); + if ( availW > 0 && availW < 1e9f && + posX + blockSize.getWidth() > availW + 0.01f ) { + Float maxBottom = curY; + for ( auto& f : leftFloats ) + maxBottom = std::max( maxBottom, f.Bottom ); + for ( auto& f : rightFloats ) + maxBottom = std::max( maxBottom, f.Bottom ); + if ( maxBottom > curY ) { + maxWidth = std::max( maxWidth, curX ); + mLines.push_back( RenderParagraph() ); + curX = 0; + curY = maxBottom; + posX = floatLeftEdge( curY ); + } + } } else { Float re = floatRightEdge( curY ); posX = re - blockSize.getWidth(); + if ( blockSize.getWidth() > re - le + 0.01f ) { + Float maxBottom = curY; + for ( auto& f : leftFloats ) + maxBottom = std::max( maxBottom, f.Bottom ); + for ( auto& f : rightFloats ) + maxBottom = std::max( maxBottom, f.Bottom ); + if ( maxBottom > curY ) { + maxWidth = std::max( maxWidth, curX ); + mLines.push_back( RenderParagraph() ); + curX = 0; + curY = maxBottom; + re = floatRightEdge( curY ); + le = floatLeftEdge( curY ); + posX = re - blockSize.getWidth(); + } + } if ( posX < le ) posX = le; } @@ -955,19 +1028,44 @@ void RichText::updateLayout() { rightFloats.push_back( fr ); } else { // ── Normal (non-float) block ──────────────────── + Float flowX = curX; if ( curX < le ) curX = le; - // Block elements force a line break before. - if ( isBlock && curX > 0 ) { - maxWidth = std::max( maxWidth, curX ); + // Block elements force a line break before + // only when there is inline-flow content on the line. + if ( isBlock && flowX > 0 ) { + maxWidth = std::max( maxWidth, flowX ); mLines.push_back( RenderParagraph() ); curX = 0; + if ( curX < le ) + curX = le; + } + + Float effW = effectiveMaxWidthAt( curY ); + + // When a block does not fit beside active floats, + // advance curY below them. + if ( isBlock && effW > 0 && effW < 1e9f && + curX + blockSize.getWidth() > effW + 0.01f && curX > 0 ) { + Float maxBottom = curY; + for ( auto& f : leftFloats ) + maxBottom = std::max( maxBottom, f.Bottom ); + for ( auto& f : rightFloats ) + maxBottom = std::max( maxBottom, f.Bottom ); + if ( maxBottom > curY ) { + maxWidth = std::max( maxWidth, curX ); + mLines.push_back( RenderParagraph() ); + curX = 0; + curY = maxBottom; + le = floatLeftEdge( curY ); + if ( curX < le ) + curX = le; + } } // 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 ); diff --git a/src/eepp/ui/uirichtext.cpp b/src/eepp/ui/uirichtext.cpp index 8ca72ba4c..2da6f45bb 100644 --- a/src/eepp/ui/uirichtext.cpp +++ b/src/eepp/ui/uirichtext.cpp @@ -1032,12 +1032,15 @@ void UIRichText::rebuildRichText( UILayout* container, RichText& richText, Intri CSSDisplay display = widget->asType()->getDisplay(); if ( display == CSSDisplay::Inline || display == CSSDisplay::InlineBlock ) isBlock = false; - else if ( display == CSSDisplay::ListItem ) + else if ( display != CSSDisplay::None ) isBlock = true; } + bool fillParent = + isBlock && widget->getLayoutWidthPolicy() == SizePolicy::MatchParent; + if ( mode == IntrinsicMode::None ) { - if ( isBlock ) { + if ( fillParent ) { if ( container->getPixelsSize().getWidth() != 0 ) { Float maxSize = eemax( 0.f, container->getPixelsSize().getWidth() - @@ -1070,7 +1073,7 @@ void UIRichText::rebuildRichText( UILayout* container, RichText& richText, Intri } Float w = size.getWidth(); - if ( isBlock && mode == IntrinsicMode::None && + if ( fillParent && mode == IntrinsicMode::None && container->getPixelsSize().getWidth() != 0 ) { w = eemax( 0.f, container->getPixelsSize().getWidth() - container->getPixelsContentOffset().Left - diff --git a/src/tests/unit_tests/richtext_tests.cpp b/src/tests/unit_tests/richtext_tests.cpp index 069740e89..7ab26093f 100644 --- a/src/tests/unit_tests/richtext_tests.cpp +++ b/src/tests/unit_tests/richtext_tests.cpp @@ -15,8 +15,8 @@ #include #include #include -#include #include +#include #include #include @@ -29,8 +29,8 @@ using namespace EE::UI::Tools; static UI::UISceneNode* createRichTextScene() { Engine::instance()->createWindow( WindowSettings( 800, 600, "RichText Test", - WindowStyle::Default, WindowBackend::Default, - 32, {}, 1, false, true ) ); + WindowStyle::Default, WindowBackend::Default, + 32, {}, 1, false, true ) ); FileSystem::changeWorkingDirectory( Sys::getProcessPath() ); FontTrueType* font = FontTrueType::New( "NotoSans-Regular" ); @@ -837,25 +837,19 @@ UTEST( UIRichText, MarginsTest ) { // Check the layout position of the second div Vector2f pos2 = d2->getPixelsPosition(); - // The widgets flow inline (horizontally) since total width < 800. + // Block elements each occupy their own line; d2 sits below d1 at the same x. // d1 footprint width: 40 (left) + 50 (width) + 20 (right) = 110. - // d2 left margin: 5. - // Therefore d2 x position = 110 + 5 = 115. - // Line height is determined by max footprint height. // d1 footprint height: 10 + 50 + 30 = 90. - // d2 footprint height: 5 + 50 + 5 = 60. - // Max ascent = 90. - // RichText baseline aligns elements to the bottom by default. - // d2 offsetY = 90 - 60 = 30. - // d2 y position = offsetY (30) + d2 margin top (5) = 35. - EXPECT_EQ( 115.f, pos2.x ); - EXPECT_EQ( 35.f, pos2.y ); + // d2 x = d1 left margin = 5 (its own left margin). + EXPECT_EQ( 5.f, pos2.x ); + // d2 y = d1 footprint height (90) + d2 margin top (5) = 95. + EXPECT_EQ( 95.f, pos2.y ); // Check UIRichText bounds - // Width = d1 footprint (110) + d2 footprint (60) = 170. - // Height = line height (90). - EXPECT_EQ( 170.f, rt->getPixelsSize().getWidth() ); - EXPECT_EQ( 90.f, rt->getPixelsSize().getHeight() ); + // Width = max(d1 footprint: 110, d2 footprint: 60) = 110. + // Height = sum of line heights: d1 line 90 + d2 line 60 = 150. + EXPECT_EQ( 110.f, rt->getPixelsSize().getWidth() ); + EXPECT_EQ( 150.f, rt->getPixelsSize().getHeight() ); destroyRichTextScene( sceneNode ); } diff --git a/src/tests/unit_tests/uicss_inheritance_tests.cpp b/src/tests/unit_tests/uicss_inheritance_tests.cpp index 780d7e4de..442a254de 100644 --- a/src/tests/unit_tests/uicss_inheritance_tests.cpp +++ b/src/tests/unit_tests/uicss_inheritance_tests.cpp @@ -27,9 +27,9 @@ UTEST( CSSInheritance, HtmlXmlLoadingInheritance ) { UIApplication app( WindowSettings( 800, 600, "eepp - CSS Inheritance Test", WindowStyle::Default, WindowBackend::Default, 32 ), - UIApplication::Settings( Sys::getProcessPath() + ".." + FileSystem::getOSSlash() ), 1 ); + UIApplication::Settings( Sys::getProcessPath() + ".." + FileSystem::getOSSlash(), 1 ) ); - std::string xml = R"( + std::string xml = R"html( - - -
- Here is some normal starting text. - This is the target inline-block element. If the container gets too narrow, this solid block drops to the next line, and its internal text will wrap, making the block taller without breaking. - And here is the text that comes immediately after. It gets pushed down correctly. -
- - -)HTML"; + std::string html; + FileSystem::fileGet( "assets/html/is_inline_block.html", html ); sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( html ) ); sceneNode->update( Seconds( 1 ) ); @@ -1254,11 +1224,13 @@ UTEST( UIHTML, InlineBlockBrowserTest ) { // If it drops to the next line: EXPECT_GT( ib->getPixelsPosition().y, t1->getPixelsPosition().y ); // And t2 should be AFTER ib (either horizontally or vertically) - EXPECT_GE( t2->getPixelsPosition().y, ib->getPixelsPosition().y ); + EXPECT_GE( t2->getPixelsPosition().y, + ib->getPixelsPosition().y + ib->getPixelsSize().getHeight() ); if ( t2->getPixelsPosition().y == ib->getPixelsPosition().y ) { EXPECT_GE( t2->getPixelsPosition().x, ib->getPixelsPosition().x + ib->getPixelsSize().getWidth() ); } + EXPECT_EQ( ib->getPixelsPosition().x, t2->getPixelsPosition().x ); Engine::destroySingleton(); } @@ -1321,7 +1293,7 @@ UTEST( UIHTML, HeightExpansion_FixedDoesNotExpand ) { UI::UISceneNode* sceneNode = init_test_inline_block(); - const std::string html = R"HTML( + const std::string html = R"html( @@ -1329,7 +1301,7 @@ UTEST( UIHTML, HeightExpansion_FixedDoesNotExpand ) {
-)HTML"; +)html"; sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( html ) ); sceneNode->update( Seconds( 1 ) ); @@ -1602,3 +1574,71 @@ UTEST( UIHTML, AnchorsSizing ) { Engine::destroySingleton(); } + +static UISceneNode* createWinAndLoadHTML( std::string winName, std::string htmlPath ) { + auto win = Engine::instance()->createWindow( + WindowSettings( 1024, 653, winName, 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" ); + if ( font == nullptr || !font->loaded() ) + return nullptr; + 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( htmlPath, html ); + sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( html ) ); + win->setClearColor( Color::White ); + + win->getInput()->update(); + SceneManager::instance()->update(); + + win->clear(); + SceneManager::instance()->draw(); + win->display(); + + return sceneNode; +} + +UTEST( UIHTML, blockFlow ) { + auto sceneNode = createWinAndLoadHTML( "HTML Block Flow", "assets/html/block_flow.html" ); + ASSERT_TRUE( sceneNode != nullptr ); + auto sections = sceneNode->getRoot()->findAllByClass( "language-section" ); + + ASSERT_EQ( sections.size(), (size_t)6 ); + + // Each section is display block so we expect a single section per line + // if sections position are not equal it means that some sections are floating + Float ref = sections[0]->getPixelsPosition().x; + for ( auto section : sections ) + EXPECT_EQ( section->getPixelsPosition().x, ref ); + + Engine::destroySingleton(); +} + +UTEST( UIHTML, blockFlowFloat ) { + auto sceneNode = + createWinAndLoadHTML( "HTML Block Flow", "assets/html/block_flow_float_left.html" ); + ASSERT_TRUE( sceneNode != nullptr ); + auto sections = sceneNode->getRoot()->findAllByClass( "language-section" ); + + ASSERT_EQ( sections.size(), (size_t)6 ); + + // Each section is display block with float: left and width 48% so we expect two sections + // per line, and each odd index should be to the right + Float refLeft = sections[0]->getPixelsPosition().x; + Float refRight = sections[1]->getPixelsPosition().x; + for ( size_t idx = 0; idx < sections.size(); idx++ ) { + Float expected = idx % 2 == 0 ? refLeft : refRight; + EXPECT_EQ( sections[idx]->getPixelsPosition().x, expected ); + } + Engine::destroySingleton(); +}