From 359dc8f157070e8f1de50539d04c3bcd1cf4b291 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=ADn=20Lucas=20Golini?= Date: Thu, 14 May 2026 00:32:58 -0300 Subject: [PATCH] block and inline-block layouter fixes, added a plan for a definitive fix in block layouter. --- ...ss_block_semantics_full_compliance_plan.md | 126 +++++++++++++++++ bin/unit_tests/assets/html/block_flow.html | 87 ++++++++++++ .../assets/html/block_flow_float_left.html | 88 ++++++++++++ .../html/eepp-ui-background-atlas-pd2.webp | Bin 25604 -> 25816 bytes .../assets/html/is_inline_block.html | 26 ++++ src/eepp/graphics/richtext.cpp | 106 +++++++++++++- src/eepp/ui/uirichtext.cpp | 9 +- src/tests/unit_tests/richtext_tests.cpp | 30 ++-- .../unit_tests/uicss_inheritance_tests.cpp | 74 +++++----- src/tests/unit_tests/uihtml_float_tests.cpp | 18 +-- src/tests/unit_tests/uihtml_tests.cpp | 130 ++++++++++++------ 11 files changed, 579 insertions(+), 115 deletions(-) create mode 100644 .agent/plans/css_block_semantics_full_compliance_plan.md create mode 100644 bin/unit_tests/assets/html/block_flow.html create mode 100644 bin/unit_tests/assets/html/block_flow_float_left.html create mode 100644 bin/unit_tests/assets/html/is_inline_block.html 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 a34bc3f61172c491937364484ea02f71f9fac540..70052132eead22ea069ad4e9e1f086a1b2f462f5 100644 GIT binary patch literal 25816 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 + + + + + +
+ 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(); +}