diff --git a/.agent/rules/html-layout-architecture.md b/.agent/rules/html-layout-architecture.md
index e1d9db8e6..8c1288448 100644
--- a/.agent/rules/html-layout-architecture.md
+++ b/.agent/rules/html-layout-architecture.md
@@ -1,36 +1,144 @@
# HTML Layout Architecture
-This document describes the decoupled HTML/CSS layout engine architecture implemented in `eepp` for `UIHTMLWidget` and related classes.
+This document describes the eepp GUI HTML compatibility layer: the subset of HTML/CSS layout implemented through `UIHTMLWidget`, `UIRichText`, `RichText`, and the `UILayouter` family.
+
+The goal is browser-compatible behavior where implemented, not a parallel eepp-specific layout language. When adding HTML/CSS features, start from the relevant specification and then map that behavior onto the existing architecture.
+
+## Spec-First Requirement
+
+Agents must implement HTML/CSS features by following the official specifications first. Do not add element-specific hacks just to match one fixture if the behavior belongs to a generic CSS concept.
+
+Primary references:
+
+- HTML Living Standard: https://html.spec.whatwg.org/
+- CSS specifications index: https://www.w3.org/Style/CSS/specs.en.html
+- CSS Display: https://www.w3.org/TR/css-display-3/
+- CSS Visual Formatting Model, including inline formatting and positioning: https://www.w3.org/TR/CSS22/visuren.html
+- CSS line height and inline-block baseline rules: https://www.w3.org/TR/CSS22/visudet.html#line-height
+- CSS Lists and Counters: https://www.w3.org/TR/css-lists-3/
+
+Required workflow for new HTML/CSS behavior:
+
+- Identify the spec section that defines the behavior.
+- Implement the behavior at the correct abstraction level, usually display/layout/formatting/list/positioning, not by tag name.
+- Preserve HTML element defaults from the spec, such as `summary { display: list-item; }`.
+- Add focused tests that encode the generic behavior and, when useful, a real HTML fixture that triggered the issue.
+- If eepp intentionally supports only a subset of a spec, document the limitation close to the implementation or in this file.
## Core Concepts
-### 1. UIHTMLWidget
-`UIHTMLWidget` is the base class for all HTML-like elements. It holds parsed CSS properties (Display, Position, Float, Clear, etc.). Instead of implementing complex layout math directly, it queries a `UILayouterManager` to instantiate the appropriate `UILayouter` based on its `CSSDisplay` property.
+### UIHTMLWidget
-### 2. Layouters
-Layout math has been extracted from widgets into stateless (or locally stateful) "Layouters":
-- **BlockLayouter:** Handles `CSSDisplay::Block`. It positions block-level children vertically. For rich text, it delegates text shaping to the `RichText` engine and simply maps physical coordinates for custom inline widgets.
-- **TableLayouter:** Handles `CSSDisplay::Table`. Encapsulates HTML table column width distribution and row positioning.
-- **InlineLayouter:** Handles `CSSDisplay::Inline`. *This layouter is empty by design.* Inline formatting (like `` or ``) is completely managed by the nearest Block container (via the `RichText` engine). It acts as a no-op so standard linear layout logic doesn't override text flows.
-- **NoneLayouter:** Handles `CSSDisplay::None`. Skips all layout and rendering.
+`UIHTMLWidget` is the base class for HTML-like elements. It stores parsed CSS layout state such as `display`, `position`, `float`, `clear`, list style, and data attributes. It does not own all layout math directly. Instead, it uses `UILayouterManager` to instantiate the appropriate `UILayouter` for its `CSSDisplay`.
-### 3. The UIRichText Engine Integration
-`UIRichText` acts as the primary block container for mixed text and widget content.
-- It uses `rebuildRichText()` to recursively traverse its children.
-- Pure text nodes (`UITextSpan`, ` `) are appended to the core `RichText` engine via `RichText::addSpan()`.
-- Arbitrary inline widgets (e.g., ` `, ``, or images) are passed to the engine via `RichText::addCustomSize()`.
-- After `RichText` performs line-wrapping, `BlockLayouter` iterates over the resulting `RenderSpan`s and calls `setPixelsPosition()` on those child widgets to match where the engine placed them.
+Important responsibilities:
-#### 3.1 UITextNode
-`UITextNode` is a lightweight, non-rendering node that represents raw text content parsed from HTML/XML (`node_pcdata`). It acts as a logical text marker in the DOM tree: its text is extracted during `rebuildRichText()` and handed to the `RichText` engine via `RichText::addSpan()`. After line-wrapping, `BlockLayouter` assigns it position and size for debugging, and it is excluded from hit testing by default. `UITextNode::draw()` is a no-op — the parent `UIRichText` handles all rendering.
+- CSS property application and invalidation.
+- Out-of-flow positioning support through containing blocks.
+- Exposing a `RichText` object for elements that render mixed inline content.
-### 4. Pixel (dp) Math strictly enforced
-All layouters **MUST** use Pixel (`Px`) variants of size and padding APIs.
-- Use `getPixelsSize()`, `getPixelsPadding()`, and `getLayoutPixelsMargin()`.
-- Never use `getSize()` or `getPadding()`, as these return density-independent pixels (dp) and will cause severe calculation bugs on HiDPI displays if mixed with pixel calculations.
+### Layouters
+
+Layout math is separated from widgets into `UILayouter` implementations:
+
+- `BlockLayouter`: Handles block-like containers, including `display: block`, `inline-block`, `list-item`, `table-cell`, and the current `flex` placeholder path. It delegates inline formatting to `RichText`, then maps the resulting physical spans back to child widgets.
+- `TableLayouter`: Handles `display: table` and encapsulates HTML table column width distribution, rows, sections, and cell positioning.
+- `InlineLayouter`: A no-op layouter for true inline text-span elements. Inline formatting is owned by the nearest rich-text/block formatting context so normal widget layout does not override text flow.
+- `NoneLayouter`: Handles `display: none` by skipping layout/rendering participation.
+
+`UILayouterManager::create()` is the dispatch point. Before adding a new display mode, check whether it should create a new formatting context, participate in an existing one, or be represented as a rich-text custom box.
+
+## RichText Integration
+
+`UIRichText` is the primary container for mixed text and widget content.
+
+`UIRichText::rebuildRichText()` recursively walks normal-flow children and builds a `Graphics::RichText` stream:
+
+- Text nodes and inline text spans become `RichText::SpanBlock` entries via `addSpan()`.
+- ` ` contributes a line break.
+- Inline-blocks, list items, replaced controls, images, and block-like widgets become `RichText::CustomBlock` entries via `addCustomSize()`.
+- Out-of-flow children are skipped here and positioned later.
+
+`RichText::updateLayout()` performs line wrapping and inline formatting. `BlockLayouter::positionRichTextChildren()` then consumes `RichText::RenderSpan`s and assigns pixel positions/sizes back to DOM widgets.
+
+### UITextNode
+
+`UITextNode` is a lightweight non-rendering node for raw parsed text (`node_pcdata`). Its text is extracted during `rebuildRichText()` and rendered by the parent `UIRichText`. After wrapping, `BlockLayouter` assigns it position and size for debugging and hit-box accounting, but `UITextNode::draw()` remains a no-op.
+
+### Custom Blocks And Baselines
+
+`RichText::CustomBlock` represents atomic inline-level or block-like widgets inside the rich-text stream. It carries:
+
+- physical size,
+- float/clear state,
+- a baseline offset,
+- virtual line-break state.
+
+The default custom-block baseline is the bottom edge to preserve old behavior for generic drawables and spacers. HTML widgets that expose internal `RichText` should provide a CSS-compatible baseline derived from their last in-flow internal line box:
+
+```cpp
+margin.Top + contentOffset.Top + lastLine.y + lastLine.maxAscent
+```
+
+This is required for `display: inline-block` and for nested rich-text widgets such as `... `. A taller inline-block caused by inherited `line-height` should keep its real height but align by baseline in the parent inline formatting context.
+
+Do not fix baseline problems by special-casing individual elements, zeroing `line-height`, or changing element display defaults. The correct layer is generic inline formatting and custom-block baseline propagation.
+
+## Display And Flow Rules
+
+### Inline Content
+
+True inline content is formatted by the nearest `UIRichText`/`BlockLayouter` context. Inline widgets should not be independently positioned by ordinary widget layout.
+
+### Inline-Block
+
+`display: inline-block` creates an atomic inline-level box in the parent inline formatting context and an internal formatting context for its children. It should:
+
+- participate in the parent line as a custom block,
+- preserve its own internal line-height and content sizing,
+- expose its internal baseline when it has in-flow line boxes,
+- fallback to its bottom edge only when it has no in-flow line boxes.
+
+### List Items And Markers
+
+`display: list-item` is block-like for layout but has marker behavior. Shared marker rendering belongs in `UIHTMLListStyle` and related list-item/summary code, not duplicated per element.
+
+Requirements:
+
+- `list-style-type` must be parsed and rendered according to the supported CSS list values.
+- `list-style-type: none` disables marker rendering and marker spacing.
+- `` defaults to `display: list-item` and uses disclosure marker defaults as defined by HTML rendering rules.
+- `disclosure-open` and `disclosure-closed` should use eepp's primitive marker drawing facilities, not textual `v` or `>` approximations.
+
+### Out-Of-Flow Positioning
-### 5. CSS Position (Out-Of-Flow)
Elements with `position: absolute` or `position: fixed`:
-- Are ignored by standard Layouters and `UIRichText::rebuildRichText()`.
-- Are positioned at the end of the parent's `updateLayout()` using `positionOutOfFlowChildren()`.
-- Absolute elements are positioned relative to their `getContainingBlock()` (the nearest positioned ancestor). Fixed elements map to the `UISceneNode` root.
+
+- are ignored by standard layouters and `UIRichText::rebuildRichText()`,
+- do not contribute to normal-flow auto size,
+- are positioned at the end of the parent's `updateLayout()` using `positionOutOfFlowChildren()`,
+- use `getContainingBlock()` for absolute positioning and the `UISceneNode` root for fixed positioning.
+
+Relative positioning should preserve normal-flow space and then offset painting/positioning according to CSS semantics.
+
+Floats are represented in `RichText::CustomBlock` with `CSSFloat`/`CSSClear` metadata and handled by the float-aware RichText path. Float placement is edge-aligned and should not be altered by inline baseline alignment.
+
+## Pixel Math
+
+All layouters must use pixel (`Px`) APIs for layout calculations:
+
+- Use `getPixelsSize()`, `getPixelsPadding()`, `getPixelsContentOffset()`, and `getLayoutPixelsMargin()`.
+- Do not mix these with `getSize()` or `getPadding()` in layout math. Those return density-independent pixels (`dp`) and will break HiDPI calculations.
+
+Convert only at clear API boundaries where the surrounding code expects dp.
+
+## Testing Expectations
+
+For HTML/CSS layout work, prefer narrow tests plus one realistic fixture when the bug came from real content:
+
+- Unit-level tests for parser/style/layout primitives.
+- RichText tests for inline formatting, wrapping, baselines, custom blocks, floats, and line-height.
+- UIHTML fixture tests for browser-like element interactions such as details/summary, tables, forms, lists, images, and positioned descendants.
+- Regression assertions should verify layout invariants, not screenshot pixels only.
+
+When possible, compare against browser behavior manually or with a reference capture, then encode the spec behavior in assertions.
diff --git a/include/eepp/graphics/richtext.hpp b/include/eepp/graphics/richtext.hpp
index 3185e52e1..159ab398d 100644
--- a/include/eepp/graphics/richtext.hpp
+++ b/include/eepp/graphics/richtext.hpp
@@ -85,6 +85,7 @@ class EE_API RichText : public Drawable {
Sizef size;
UI::CSSFloat floatType{ UI::CSSFloat::None };
UI::CSSClear clearType{ UI::CSSClear::None };
+ Float baseline{ 0 };
bool isLineBreak{ false };
};
@@ -108,7 +109,7 @@ class EE_API RichText : public Drawable {
* @param size The physical dimensions of the spacer.
*/
void addCustomSize( const Sizef& size, UI::CSSFloat floatType = UI::CSSFloat::None,
- UI::CSSClear clearType = UI::CSSClear::None );
+ UI::CSSClear clearType = UI::CSSClear::None, Float baseline = -1.f );
/** @brief Adds a virtual line break that is not associated with a DOM text character. */
void addLineBreak();
diff --git a/src/eepp/graphics/richtext.cpp b/src/eepp/graphics/richtext.cpp
index 24f238ebd..e33c2909a 100644
--- a/src/eepp/graphics/richtext.cpp
+++ b/src/eepp/graphics/richtext.cpp
@@ -320,13 +320,16 @@ void RichText::addDrawable( std::shared_ptr drawable ) {
invalidateLayout();
}
-void RichText::addCustomSize( const Sizef& size, UI::CSSFloat floatType, UI::CSSClear clearType ) {
- mBlocks.push_back( CustomBlock{ size, floatType, clearType, false } );
+void RichText::addCustomSize( const Sizef& size, UI::CSSFloat floatType, UI::CSSClear clearType,
+ Float baseline ) {
+ mBlocks.push_back( CustomBlock{ size, floatType, clearType,
+ baseline >= 0.f ? baseline : size.getHeight(), false } );
invalidateLayout();
}
void RichText::addLineBreak() {
- mBlocks.push_back( CustomBlock{ Sizef::Zero, UI::CSSFloat::None, UI::CSSClear::None, true } );
+ mBlocks.push_back(
+ CustomBlock{ Sizef::Zero, UI::CSSFloat::None, UI::CSSClear::None, 0.f, true } );
invalidateLayout();
}
@@ -592,12 +595,15 @@ void RichText::updateLayout() {
} else {
// Drawable or CustomBlock (non-float).
Sizef blockSize;
+ Float blockBaseline = 0.f;
bool isLineBreak = false;
if ( auto pDrawable = std::get_if>( &block ) ) {
auto& drawable = *pDrawable;
blockSize = drawable ? drawable->getPixelsSize() : Sizef();
+ blockBaseline = blockSize.getHeight();
} else if ( auto pSize = std::get_if( &block ) ) {
blockSize = pSize->size;
+ blockBaseline = pSize->baseline;
isLineBreak = pSize->isLineBreak;
}
@@ -624,7 +630,7 @@ void RichText::updateLayout() {
RenderParagraph& currentLine = mLines.back();
currentLine.spans.push_back( renderSpan );
- currentLine.maxAscent = std::max( currentLine.maxAscent, blockSize.getHeight() );
+ currentLine.maxAscent = std::max( currentLine.maxAscent, blockBaseline );
currentLine.height = std::max( currentLine.height, blockSize.getHeight() );
curX += blockSize.getWidth();
@@ -671,7 +677,10 @@ void RichText::updateLayout() {
span.position.y = offsetY;
maxLineHeight = std::max( maxLineHeight, offsetY + span.size.getHeight() );
} else {
- Float offsetY = line.maxAscent - span.size.getHeight();
+ Float baseline = span.size.getHeight();
+ if ( auto pSize = std::get_if( &span.block ) )
+ baseline = pSize->baseline;
+ Float offsetY = line.maxAscent - baseline;
if ( offsetY < 0 )
offsetY = 0;
span.position.x += xOffset;
@@ -886,14 +895,17 @@ void RichText::updateLayout() {
} else {
// ── Drawable or CustomBlock ────────────────────────────
Sizef blockSize;
+ Float blockBaseline = 0.f;
bool isLineBreak = false;
UI::CSSFloat floatType = UI::CSSFloat::None;
UI::CSSClear clearType = UI::CSSClear::None;
if ( auto pDrawable = std::get_if>( &block ) ) {
auto& drawable = *pDrawable;
blockSize = drawable ? drawable->getPixelsSize() : Sizef();
+ blockBaseline = blockSize.getHeight();
} else if ( auto pSize = std::get_if( &block ) ) {
blockSize = pSize->size;
+ blockBaseline = pSize->baseline;
floatType = pSize->floatType;
clearType = pSize->clearType;
isLineBreak = pSize->isLineBreak;
@@ -1021,7 +1033,7 @@ void RichText::updateLayout() {
RenderParagraph& currentLine = mLines.back();
currentLine.spans.push_back( renderSpan );
- currentLine.maxAscent = std::max( currentLine.maxAscent, blockSize.getHeight() );
+ currentLine.maxAscent = std::max( currentLine.maxAscent, blockBaseline );
currentLine.height = std::max( currentLine.height, blockSize.getHeight() );
curX += blockSize.getWidth();
@@ -1075,7 +1087,10 @@ void RichText::updateLayout() {
span.position.y = offsetY;
maxLineHeight = std::max( maxLineHeight, offsetY + span.size.getHeight() );
} else {
- Float offsetY = line.maxAscent - span.size.getHeight();
+ Float baseline = span.size.getHeight();
+ if ( auto pSize = std::get_if( &span.block ) )
+ baseline = pSize->baseline;
+ Float offsetY = line.maxAscent - baseline;
if ( offsetY < 0 )
offsetY = 0;
// Float spans keep their edge-aligned x; only inline-flow spans shift.
diff --git a/src/eepp/ui/blocklayouter.cpp b/src/eepp/ui/blocklayouter.cpp
index 914303510..cde22a0fb 100644
--- a/src/eepp/ui/blocklayouter.cpp
+++ b/src/eepp/ui/blocklayouter.cpp
@@ -243,8 +243,7 @@ void BlockLayouter::positionRichTextChildren( Graphics::RichText* rt ) {
bool handled = false;
- if ( widget->isType( UI_TYPE_HTML_WIDGET ) &&
- widget->asType()->isInline() ) {
+ if ( widget->isType( UI_TYPE_HTML_WIDGET ) && widget->asType()->isInline() ) {
UITextSpan* textSpan = widget->asType();
Int64 startChar = curCharIdx;
Int64 endChar = curCharIdx;
@@ -351,6 +350,23 @@ void BlockLayouter::positionRichTextChildren( Graphics::RichText* rt ) {
span->position.y + margin.Top );
widget->setPixelsPosition( targetPos - offset );
+ if ( widget->getLayoutWidthPolicy() == SizePolicy::MatchParent &&
+ mContainer->getLayoutWidthPolicy() == SizePolicy::WrapContent ) {
+ // Stretch match-parent children only after the wrap-content parent has its
+ // final used width. During RichText measurement this width may still be a
+ // stale pre-style value, so using it there would poison shrink-to-fit
+ // inline-block sizing.
+ Float contentWidth =
+ eemax( 0.f, mContainer->getPixelsSize().getWidth() -
+ mContainer->getPixelsContentOffset().Left -
+ mContainer->getPixelsContentOffset().Right -
+ margin.Left - margin.Right );
+ if ( contentWidth != widget->getPixelsSize().getWidth() ) {
+ widget->setPixelsSize( contentWidth,
+ widget->getPixelsSize().getHeight() );
+ mResizedCount++;
+ }
+ }
bounds = Rectf( targetPos, span->size );
}
diff --git a/src/eepp/ui/uirichtext.cpp b/src/eepp/ui/uirichtext.cpp
index eb8dc4455..cb063e1a9 100644
--- a/src/eepp/ui/uirichtext.cpp
+++ b/src/eepp/ui/uirichtext.cpp
@@ -821,6 +821,27 @@ String UIRichText::UIRichText::collapseInternalWhitespace( const String& s ) {
return out;
}
+static Float getCustomBlockBaseline( UIWidget* widget, const Sizef& widgetSize,
+ const Rectf& margin ) {
+ Float fallbackBaseline = widgetSize.getHeight() + margin.Top + margin.Bottom;
+ if ( !widget->isType( UI_TYPE_HTML_WIDGET ) )
+ return fallbackBaseline;
+
+ auto* htmlWidget = widget->asType();
+ auto* rt = htmlWidget->getRichTextPtr();
+ if ( rt == nullptr )
+ return fallbackBaseline;
+
+ rt->updateLayout();
+ const auto& lines = rt->getLines();
+ for ( auto it = lines.rbegin(); it != lines.rend(); ++it ) {
+ if ( !it->spans.empty() )
+ return margin.Top + widget->getPixelsContentOffset().Top + it->y + it->maxAscent;
+ }
+
+ return fallbackBaseline;
+}
+
void UIRichText::rebuildRichText( UILayout* container, RichText& richText, IntrinsicMode mode ) {
richText.clear();
if ( container->isType( UI_TYPE_RICHTEXT ) ) {
@@ -973,8 +994,7 @@ void UIRichText::rebuildRichText( UILayout* container, RichText& richText, Intri
bool handled = false;
- if ( widget->isType( UI_TYPE_HTML_WIDGET ) &&
- widget->asType()->isInline() ) {
+ if ( widget->isType( UI_TYPE_HTML_WIDGET ) && widget->asType()->isInline() ) {
UITextSpan* span = widget->asType();
span->setLayoutCharCount( 0 );
Rectf margin = span->getLayoutPixelsMargin();
@@ -1067,7 +1087,8 @@ void UIRichText::rebuildRichText( UILayout* container, RichText& richText, Intri
if ( mode == IntrinsicMode::None ) {
if ( fillParent ) {
- if ( container->getPixelsSize().getWidth() != 0 ) {
+ if ( container->getLayoutWidthPolicy() != SizePolicy::WrapContent &&
+ container->getPixelsSize().getWidth() != 0 ) {
Float maxSize =
eemax( 0.f, container->getPixelsSize().getWidth() -
container->getPixelsContentOffset().Left -
@@ -1099,6 +1120,7 @@ void UIRichText::rebuildRichText( UILayout* container, RichText& richText, Intri
Float w = size.getWidth();
if ( fillParent && mode == IntrinsicMode::None &&
+ container->getLayoutWidthPolicy() != SizePolicy::WrapContent &&
container->getPixelsSize().getWidth() != 0 ) {
w = eemax( 0.f, container->getPixelsSize().getWidth() -
container->getPixelsContentOffset().Left -
@@ -1117,9 +1139,10 @@ void UIRichText::rebuildRichText( UILayout* container, RichText& richText, Intri
if ( isNormalFlowBlock )
richText.addLineBreak();
- richText.addCustomSize( Sizef( w + margin.Left + margin.Right,
- size.getHeight() + margin.Top + margin.Bottom ),
- floatType, clearType );
+ Sizef customSize( w + margin.Left + margin.Right,
+ size.getHeight() + margin.Top + margin.Bottom );
+ richText.addCustomSize( customSize, floatType, clearType,
+ getCustomBlockBaseline( widget, size, margin ) );
if ( widget->isType( UI_TYPE_TEXTSPAN ) &&
widget->asType()->isInlineBlock() &&
diff --git a/src/tests/unit_tests/richtext_tests.cpp b/src/tests/unit_tests/richtext_tests.cpp
index 96cebf282..483c3cdb3 100644
--- a/src/tests/unit_tests/richtext_tests.cpp
+++ b/src/tests/unit_tests/richtext_tests.cpp
@@ -219,6 +219,37 @@ UTEST( RichText, BaselineAlignment ) {
Engine::destroySingleton();
}
+UTEST( RichText, CustomBlockBaselineAlignment ) {
+ Engine::instance()->createWindow( WindowSettings( 800, 600, "RichText Custom Block Baseline",
+ WindowStyle::Default, WindowBackend::Default,
+ 32, {}, 1, false, true ) );
+ FileSystem::changeWorkingDirectory( Sys::getProcessPath() );
+
+ FontTrueType* font = FontTrueType::New( "NotoSans-Regular" );
+ font->loadFromFile( "../assets/fonts/NotoSans-Regular.ttf" );
+ ASSERT_TRUE( font->loaded() );
+
+ RichText richText;
+ richText.getFontStyleConfig().Font = font;
+ richText.addSpan( "Large", nullptr, 30 );
+ richText.addSpan( "Small", nullptr, 12 );
+ richText.addCustomSize( Sizef( 24, 30 ), UI::CSSFloat::None, UI::CSSClear::None, 12.f );
+
+ richText.getSize();
+
+ const auto& lines = richText.getLines();
+ ASSERT_EQ( lines.size(), (size_t)1 );
+ ASSERT_EQ( lines[0].spans.size(), (size_t)3 );
+
+ const auto& smallSpan = lines[0].spans[1];
+ const auto& customSpan = lines[0].spans[2];
+
+ EXPECT_NEAR( customSpan.position.y, smallSpan.position.y, 0.001f );
+ EXPECT_GT( customSpan.position.y, 0.f );
+
+ Engine::destroySingleton();
+}
+
UTEST( LineWrap, SoftWrapPreventsWordSplitWithOffset ) {
Engine::instance()->createWindow( WindowSettings( 800, 600, "LineWrap Test",
WindowStyle::Default, WindowBackend::Default,
diff --git a/src/tests/unit_tests/uihtml_tests.cpp b/src/tests/unit_tests/uihtml_tests.cpp
index 757ea6bc6..38aa361da 100644
--- a/src/tests/unit_tests/uihtml_tests.cpp
+++ b/src/tests/unit_tests/uihtml_tests.cpp
@@ -684,6 +684,8 @@ UTEST( UIHTMLDetails, inlineBlockSummaryListStyleNoneSize ) {
EXPECT_LE( details->getPixelsSize().getHeight(),
eemax( author->getPixelsSize().getHeight(), time->getPixelsSize().getHeight() ) +
1.f );
+ EXPECT_NEAR( details->getPixelsPosition().y, author->getPixelsPosition().y, 2.f );
+ EXPECT_NEAR( details->getPixelsPosition().y, time->getPixelsPosition().y, 2.f );
Engine::destroySingleton();
}
@@ -1342,6 +1344,185 @@ UTEST( UILayout, listStyleInheritanceFromUl ) {
Engine::destroySingleton();
}
+UTEST( UIHTMLDetails, lobstersInlineBlockCachesWidth ) {
+ Engine::instance()->createWindow( WindowSettings( 424, 184, "HTML Details Lobsters Test",
+ WindowStyle::Default, WindowBackend::Default,
+ 32, {}, 1, false, true ) );
+ FileSystem::changeWorkingDirectory( Sys::getProcessPath() );
+
+ FontTrueType* font = FontTrueType::New( "NotoSans-Regular" );
+ font->loadFromFile( "../assets/fonts/NotoSans-Regular.ttf" );
+ FontFamily::loadFromRegular( font );
+
+ UI::UISceneNode* sceneNode = UI::UISceneNode::New();
+ SceneManager::instance()->add( sceneNode );
+ UI::UIThemeManager* themeManager = sceneNode->getUIThemeManager();
+ themeManager->setDefaultFont( font );
+ sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( R"html(
+
+
+
+
+
+
+
+ )html" ) );
+ sceneNode->updateDirtyLayouts();
+
+ sceneNode->combineStyleSheet( R"css(
+ body, textarea, input, button {
+ font-family: "helvetica neue", arial, sans-serif;
+ line-height: 1.45em;
+ }
+ ol.stories {
+ padding: 0;
+ list-style: none;
+ margin: 0;
+ }
+ div.voters {
+ float: left;
+ text-align: center;
+ width: 40px;
+ }
+ li.story {
+ clear: both;
+ }
+ ol.stories li.story div.story_liner {
+ padding-top: 0.25em;
+ padding-bottom: 0.25em;
+ word-break: break-word;
+ }
+ li div.details {
+ padding-top: 0.1em;
+ margin-left: 32px;
+ }
+ li .link {
+ font-weight: bold;
+ vertical-align: middle;
+ }
+ li .link a {
+ text-decoration: none;
+ }
+ li.story a.tag {
+ vertical-align: middle;
+ }
+ li .tags {
+ margin-right: 0.25em;
+ }
+ li .domain {
+ font-style: italic;
+ text-decoration: none;
+ vertical-align: middle;
+ }
+ img.avatar {
+ border-radius: 8px;
+ height: 16px;
+ margin-bottom: 2px;
+ margin-right: 2px;
+ vertical-align: middle;
+ width: 16px;
+ }
+ li.story .byline {
+ margin-top: 1px;
+ }
+ .caches {
+ display: inline-block;
+ position: relative;
+ }
+ .caches summary {
+ list-style: none;
+ }
+ .caches ul {
+ position: absolute;
+ white-space: nowrap;
+ list-style: none;
+ padding: 0;
+ z-index: 1;
+ }
+ .caches a {
+ text-decoration: none;
+ display: block;
+ padding: 3px 7px;
+ }
+ @media only screen and (max-width: 480px) {
+ div#inside {
+ margin: 0.5rem;
+ }
+ ol.stories {
+ margin: 0 0 0 -0.5rem;
+ padding-left: 0;
+ }
+ div.voters {
+ margin-left: 0.25em;
+ margin-top: 0px;
+ width: 30px;
+ }
+ ol.stories.list {
+ margin-top: 0;
+ }
+ ol.stories.list li.story {
+ display: table;
+ }
+ ol.stories.list li.story div.story_liner {
+ display: table-cell;
+ padding-top: 0.5em;
+ padding-bottom: 0.75em;
+ width: 100%;
+ }
+ li div.details {
+ margin: 0 0 0 36px;
+ }
+ }
+ )css" );
+ sceneNode->updateDirtyLayouts();
+
+ auto* caches = sceneNode->getRoot()->querySelector( ".caches" )->asType();
+ auto* comments = sceneNode->getRoot()->querySelector( ".comments_label" );
+ auto* byline = sceneNode->getRoot()->querySelector( ".byline" );
+
+ EXPECT_LT( caches->getPixelsSize().getWidth(), byline->getPixelsSize().getWidth() * 0.5f );
+ EXPECT_NEAR( caches->getPixelsPosition().y, comments->getPixelsPosition().y, 1.f );
+
+ Engine::destroySingleton();
+}
+
UTEST( UIBorder, renderingVariations ) {
auto win = Engine::instance()->createWindow(
WindowSettings( 1024, 653, "Border Rendering Test", WindowStyle::Default,