block and inline-block layouter fixes, added a plan for a definitive fix in block layouter.

This commit is contained in:
Martín Lucas Golini
2026-05-14 00:32:58 -03:00
parent 4b58b2f61b
commit 359dc8f157
11 changed files with 579 additions and 115 deletions

View File

@@ -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 | `<richtext>` widgets and `<div>` containers are both `UIRichText` with `CSSDisplay::Block`, yet `<richtext>` 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.

View File

@@ -0,0 +1,87 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>Block Layout</title>
<style>
body {
background-color: #f8f9fa;
}
h1 {
text-align: center;
color: #222;
}
.container {
max-width: 1400px;
margin: 0 auto;
}
.language-section {
width: 48%;
margin: 4px;
padding: 8px;
border: 1px solid #ddd;
border-radius: 8px;
background-color: white;
box-sizing: border-box;
}
.lang-name {
font-size: 1.25em;
font-weight: bold;
margin-bottom: 12px;
color: #0066cc;
}
ul {
list-style-type: none;
padding: 0;
margin: 0;
}
li {
margin: 5px 0;
font-size: 1.05em;
}
.rtl {
direction: rtl;
text-align: right;
}
</style>
</head>
<body>
<div class="container">
<div class="language-section">
<div class="lang-name">English</div>
<ul>
<li>Hello</li>
</ul>
</div>
<div class="language-section">
<div class="lang-name">Chinese</div>
<ul>
<li>Nǐ hǎo</li>
</ul>
</div>
<div class="language-section">
<div class="lang-name">Japanese</div>
<ul>
<li>Konnichiwa</li>
</ul>
</div>
<div class="language-section rtl">
<div class="lang-name">Arabic</div>
<ul>
<li>Marhaban</li>
</ul>
</div>
<div class="language-section rtl">
<div class="lang-name">Hebrew</div>
<ul>
<li>Shalom</li>
</ul>
</div>
<div class="language-section">
<div class="lang-name">Russian</div>
<ul>
<li>Privet</li>
</ul>
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,88 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>Block Layout</title>
<style>
body {
background-color: #f8f9fa;
}
h1 {
text-align: center;
color: #222;
}
.container {
max-width: 1400px;
margin: 0 auto;
}
.language-section {
float: left;
width: 48%;
margin: 4px;
padding: 8px;
border: 1px solid #ddd;
border-radius: 8px;
background-color: white;
box-sizing: border-box;
}
.lang-name {
font-size: 1.25em;
font-weight: bold;
margin-bottom: 12px;
color: #0066cc;
}
ul {
list-style-type: none;
padding: 0;
margin: 0;
}
li {
margin: 5px 0;
font-size: 1.05em;
}
.rtl {
direction: rtl;
text-align: right;
}
</style>
</head>
<body>
<div class="container">
<div class="language-section">
<div class="lang-name">English</div>
<ul>
<li>Hello</li>
</ul>
</div>
<div class="language-section">
<div class="lang-name">Chinese</div>
<ul>
<li>Nǐ hǎo</li>
</ul>
</div>
<div class="language-section">
<div class="lang-name">Japanese</div>
<ul>
<li>Konnichiwa</li>
</ul>
</div>
<div class="language-section rtl">
<div class="lang-name">Arabic</div>
<ul>
<li>Marhaban</li>
</ul>
</div>
<div class="language-section rtl">
<div class="lang-name">Hebrew</div>
<ul>
<li>Shalom</li>
</ul>
</div>
<div class="language-section">
<div class="lang-name">Russian</div>
<ul>
<li>Privet</li>
</ul>
</div>
</div>
</body>
</html>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 25 KiB

View File

@@ -0,0 +1,26 @@
<!DOCTYPE html>
<html lang="en">
<head>
<style>
.parent-container {
width: 450px;
}
.target {
background-color: #e0f7fa;
}
.is-inline-block {
display: inline-block;
}
.is-inline {
display: inline;
}
</style>
</head>
<body>
<div class="parent-container" id="parent-ib">
<span id="t1">Here is some normal starting text.</span>
<span id="ib" class="target is-inline-block">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.</span>
<span id="t2">And here is the text that comes immediately after. It gets pushed down correctly.</span>
</div>
</body>
</html>

View File

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

View File

@@ -1032,12 +1032,15 @@ void UIRichText::rebuildRichText( UILayout* container, RichText& richText, Intri
CSSDisplay display = widget->asType<UIHTMLWidget>()->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 -

View File

@@ -15,8 +15,8 @@
#include <eepp/ui/uilinearlayout.hpp>
#include <eepp/ui/uirichtext.hpp>
#include <eepp/ui/uiscenenode.hpp>
#include <eepp/ui/uitextspan.hpp>
#include <eepp/ui/uitextnode.hpp>
#include <eepp/ui/uitextspan.hpp>
#include <eepp/ui/uithememanager.hpp>
#include <eepp/window/engine.hpp>
@@ -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 );
}

View File

@@ -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(
<html>
<head>
<style>
@@ -43,7 +43,7 @@ UTEST( CSSInheritance, HtmlXmlLoadingInheritance ) {
<div id="testdiv">This is not color black</div>
</body>
</html>
)";
)html";
UIWidget* root = app.getUI()->loadLayoutFromString( xml );
EXPECT_TRUE( root != nullptr );
@@ -66,7 +66,7 @@ UTEST( CSSInheritance, ComputedFontSize ) {
UIApplication::Settings(
Sys::getProcessPath() + ".." + FileSystem::getOSSlash(), scale ) );
std::string xml = R"(
std::string xml = R"html(
<html>
<head>
<style>
@@ -82,7 +82,7 @@ UTEST( CSSInheritance, ComputedFontSize ) {
<h1 id="testh1">test text</h1>
</body>
</html>
)";
)html";
UIWidget* root = app.getUI()->loadLayoutFromString( xml );
EXPECT_TRUE( root != nullptr );
@@ -114,7 +114,7 @@ UTEST( CSSInheritance, ComputedFontSizePercentageAndRem ) {
UIApplication::Settings(
Sys::getProcessPath() + ".." + FileSystem::getOSSlash(), scale ) );
std::string xml = R"(
std::string xml = R"html(
<html>
<head>
<style>
@@ -145,7 +145,7 @@ UTEST( CSSInheritance, ComputedFontSizePercentageAndRem ) {
</div>
</body>
</html>
)";
)html";
UIWidget* root = app.getUI()->loadLayoutFromString( xml );
EXPECT_TRUE( root != nullptr );
@@ -161,9 +161,9 @@ UTEST( CSSInheritance, ExplicitColorInherit ) {
UIApplication app(
WindowSettings( 800, 600, "eepp - CSS Color Inherit 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(
<html>
<head>
<style>
@@ -179,7 +179,7 @@ UTEST( CSSInheritance, ExplicitColorInherit ) {
<div>color set on body<div id="child">should be red via inherit</div></div>
</body>
</html>
)";
)html";
UIWidget* root = app.getUI()->loadLayoutFromString( xml );
EXPECT_TRUE( root != nullptr );
@@ -198,7 +198,7 @@ UTEST( CSSInheritance, ExplicitFontSizeInherit ) {
UIApplication::Settings(
Sys::getProcessPath() + ".." + FileSystem::getOSSlash(), scale ) );
std::string xml = R"(
std::string xml = R"html(
<html>
<head>
<style>
@@ -217,7 +217,7 @@ UTEST( CSSInheritance, ExplicitFontSizeInherit ) {
<div id="parent">parent<div id="child">child with inherit</div></div>
</body>
</html>
)";
)html";
UIWidget* root = app.getUI()->loadLayoutFromString( xml );
EXPECT_TRUE( root != nullptr );
@@ -236,7 +236,7 @@ UTEST( CSSInheritance, ExplicitFontSizeInheritEm ) {
UIApplication::Settings(
Sys::getProcessPath() + ".." + FileSystem::getOSSlash(), scale ) );
std::string xml = R"(
std::string xml = R"html(
<html>
<head>
<style>
@@ -255,7 +255,7 @@ UTEST( CSSInheritance, ExplicitFontSizeInheritEm ) {
<div id="parent">parent (1.5em = 36px)<div id="child">inherit should be 36px</div></div>
</body>
</html>
)";
)html";
UIWidget* root = app.getUI()->loadLayoutFromString( xml );
EXPECT_TRUE( root != nullptr );
@@ -271,9 +271,9 @@ UTEST( CSSInheritance, ExplicitFontFamilyInherit ) {
UIApplication app(
WindowSettings( 800, 600, "eepp - CSS FontFamily Inherit 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(
<html>
<head>
<style>
@@ -289,7 +289,7 @@ UTEST( CSSInheritance, ExplicitFontFamilyInherit ) {
<div id="parent"><div id="child">child with font-family: inherit</div></div>
</body>
</html>
)";
)html";
UIWidget* root = app.getUI()->loadLayoutFromString( xml );
EXPECT_TRUE( root != nullptr );
@@ -308,9 +308,9 @@ UTEST( CSSInheritance, ExplicitBackgroundColorInherit ) {
UIApplication app(
WindowSettings( 800, 600, "eepp - CSS BGColor Inherit 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(
<html>
<head>
<style>
@@ -326,7 +326,7 @@ UTEST( CSSInheritance, ExplicitBackgroundColorInherit ) {
<div id="child">child with background-color: inherit</div>
</body>
</html>
)";
)html";
UIWidget* root = app.getUI()->loadLayoutFromString( xml );
EXPECT_TRUE( root != nullptr );
@@ -353,7 +353,7 @@ UTEST( CSSUnits, ExChWithFont ) {
UIApplication app(
WindowSettings( 800, 600, "eepp - CSS Units Ex/Ch Test", WindowStyle::Default,
WindowBackend::Default, 32 ),
UIApplication::Settings( Sys::getProcessPath() + ".." + FileSystem::getOSSlash() ), 1.f );
UIApplication::Settings( Sys::getProcessPath() + ".." + FileSystem::getOSSlash(), 1 ) );
Graphics::Font* font = app.getUI()->getUIThemeManager()->getDefaultFont();
EXPECT_TRUE( font != nullptr );
@@ -378,9 +378,9 @@ UTEST( CSSVariables, VariableReferencesSimple ) {
UIApplication app(
WindowSettings( 800, 600, "eepp - CSS Var Ref Test 1", 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(
<html>
<head>
<style>
@@ -395,7 +395,7 @@ UTEST( CSSVariables, VariableReferencesSimple ) {
<div id="testdiv">Test text</div>
</body>
</html>
)";
)html";
UIWidget* root = app.getUI()->loadLayoutFromString( xml );
EXPECT_TRUE( root != nullptr );
@@ -410,9 +410,9 @@ UTEST( CSSVariables, VariableReferencesChain ) {
UIApplication app(
WindowSettings( 800, 600, "eepp - CSS Var Ref Test 2", 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(
<html>
<head>
<style>
@@ -428,7 +428,7 @@ UTEST( CSSVariables, VariableReferencesChain ) {
<div id="testdiv">Test text</div>
</body>
</html>
)";
)html";
UIWidget* root = app.getUI()->loadLayoutFromString( xml );
EXPECT_TRUE( root != nullptr );
@@ -442,10 +442,10 @@ UTEST( CSSVariables, VariableReferencesChain ) {
UTEST( CSSVariables, VariableReferencesWithPadding ) {
UIApplication app(
WindowSettings( 800, 600, "eepp - CSS Var Ref Test 3", WindowStyle::Default,
WindowBackend::Default, 32 ),
UIApplication::Settings( Sys::getProcessPath() + ".." + FileSystem::getOSSlash() ), 1 );
WindowBackend::Default, 32, {}, 1 ),
UIApplication::Settings( Sys::getProcessPath() + ".." + FileSystem::getOSSlash(), 1 ) );
std::string xml = R"(
std::string xml = R"html(
<html>
<head>
<style>
@@ -460,7 +460,7 @@ UTEST( CSSVariables, VariableReferencesWithPadding ) {
<div id="testdiv">Test text</div>
</body>
</html>
)";
)html";
UIWidget* root = app.getUI()->loadLayoutFromString( xml );
EXPECT_TRUE( root != nullptr );
@@ -476,9 +476,9 @@ UTEST( CSSVariables, VariableReferencesMultiple ) {
UIApplication app(
WindowSettings( 800, 600, "eepp - CSS Var Ref Test 4", 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(
<html>
<head>
<style>
@@ -494,7 +494,7 @@ UTEST( CSSVariables, VariableReferencesMultiple ) {
<div id="testdiv">Test text</div>
</body>
</html>
)";
)html";
UIWidget* root = app.getUI()->loadLayoutFromString( xml );
EXPECT_TRUE( root != nullptr );
@@ -509,9 +509,9 @@ UTEST( CSSVariables, VariableReferencesCircular ) {
UIApplication app(
WindowSettings( 800, 600, "eepp - CSS Var Ref Test 5", 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(
<html>
<head>
<style>
@@ -526,7 +526,7 @@ UTEST( CSSVariables, VariableReferencesCircular ) {
<div id="testdiv">Test text</div>
</body>
</html>
)";
)html";
UIWidget* root = app.getUI()->loadLayoutFromString( xml );
EXPECT_TRUE( root != nullptr );

View File

@@ -8,8 +8,8 @@
#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/ui/uithememanager.hpp>
#include <eepp/window/engine.hpp>
#include <eepp/window/window.hpp>
@@ -19,9 +19,9 @@ 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 ) );
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" );
@@ -118,11 +118,13 @@ UTEST( UIHTMLFloat, richtext_NoFloatLayout_NoChange ) {
child1->setParent( container );
child1->setPixelsSize( 100, 50 );
child1->setLayoutSizePolicy( SizePolicy::Fixed, SizePolicy::Fixed );
child1->setDisplay( CSSDisplay::InlineBlock );
UIHTMLWidget* child2 = UIHTMLWidget::New();
child2->setParent( container );
child2->setPixelsSize( 150, 30 );
child2->setLayoutSizePolicy( SizePolicy::Fixed, SizePolicy::Fixed );
child2->setDisplay( CSSDisplay::InlineBlock );
sceneNode->updateDirtyLayouts();
@@ -542,10 +544,10 @@ UTEST( UIHTMLFloat, floatLeftNonHTMLwidget_NoCrash ) {
}
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 ) );
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" );

View File

@@ -971,9 +971,6 @@ static UISceneNode* init_test_inline_block() {
SceneManager::instance()->setCurrentUISceneNode( sceneNode );
UIThemeManager* themeManager = sceneNode->getUIThemeManager();
themeManager->setDefaultFont( font );
UITheme* theme = UITheme::New( "default", "default" );
theme->setDefaultFont( font );
themeManager->setDefaultTheme( theme );
themeManager->applyDefaultTheme( sceneNode->getRoot() );
return sceneNode;
}
@@ -986,7 +983,7 @@ UTEST( UIHTML, InlineBlock ) {
UISceneNode* sceneNode = init_test_inline_block();
const std::string html = R"HTML(
const std::string html = R"html(
<!DOCTYPE html>
<html>
<head>
@@ -1008,7 +1005,7 @@ ul > li {
</ul>
</body>
</html>
)HTML";
)html";
sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( html ) );
@@ -1046,12 +1043,12 @@ UTEST( UIHTML, BlockList ) {
UISceneNode* sceneNode = init_test_inline_block();
const std::string html = R"HTML(
const std::string html = R"html(
<ul id="block-list">
<li style="height: 20px">Item 1</li>
<li style="height: 20px">Item 2</li>
</ul>
)HTML";
)html";
sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( html ) );
sceneNode->update( Seconds( 1 ) );
@@ -1091,12 +1088,12 @@ UTEST( UIHTML, InlineList ) {
UISceneNode* sceneNode = init_test_inline_block();
const std::string html = R"HTML(
const std::string html = R"html(
<ul style="display: block">
<li style="display: inline">Item 1</li>
<li style="display: inline">Item 2</li>
</ul>
)HTML";
)html";
sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( html ) );
sceneNode->update( Seconds( 1 ) );
@@ -1130,12 +1127,12 @@ UTEST( UIHTML, InlineBlockExplicitWidth ) {
UISceneNode* sceneNode = init_test_inline_block();
const std::string html = R"HTML(
const std::string html = R"html(
<div style="width: 200px">
<div id="d1" style="display: inline-block; width: 150px; height: 50px"></div>
<div id="d2" style="display: inline-block; width: 150px; height: 50px"></div>
</div>
)HTML";
)html";
sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( html ) );
sceneNode->update( Seconds( 1 ) );
@@ -1159,13 +1156,13 @@ UTEST( UIHTML, InlineBlockMixedContent ) {
UISceneNode* sceneNode = init_test_inline_block();
const std::string html = R"HTML(
const std::string html = R"html(
<div>
Some text
<div id="ib" style="display: inline-block; width: 50px; height: 50px; background-color: red"></div>
more text
</div>
)HTML";
)html";
sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( html ) );
sceneNode->update( Seconds( 1 ) );
@@ -1212,35 +1209,8 @@ UTEST( UIHTML, InlineBlockBrowserTest ) {
ContextSettings( false, 0, 0, GLv_default, true, false ) );
UI::UISceneNode* sceneNode = init_test_inline_block();
const std::string html = R"HTML(
<!DOCTYPE html>
<html lang="en">
<head>
<style>
.parent-container {
width: 450px;
}
.target {
background-color: #e0f7fa;
}
.is-inline-block {
display: inline-block;
}
.is-inline {
display: inline;
}
</style>
</head>
<body>
<div class="parent-container" id="parent-ib">
<span id="t1">Here is some normal starting text.</span>
<span id="ib" class="target is-inline-block">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.</span>
<span id="t2">And here is the text that comes immediately after. It gets pushed down correctly.</span>
</div>
</body>
</html>
)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(
<!DOCTYPE html>
<html>
<body style="margin: 0; padding: 0;">
@@ -1329,7 +1301,7 @@ UTEST( UIHTML, HeightExpansion_FixedDoesNotExpand ) {
<div style="position: fixed; top: 500px; height: 50px;"></div>
</body>
</html>
)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();
}