Temporary fix until we have proper layout invalidation.

This commit is contained in:
Martín Lucas Golini
2026-06-07 02:00:42 -03:00
parent 788dcc6bf4
commit 12413ea630
5 changed files with 726 additions and 1 deletions
@@ -164,6 +164,632 @@ The state should live near `UILayout` / `UIHTMLWidget` boundaries rather than in
ad-hoc element code. HTML-specific reasons can be layered on top of the generic
bits when needed.
## Implementation Design
This section is intentionally detailed. The implementation should be incremental
and reversible, but every phase should move toward one invariant:
> Layout invalidation records what dependency became stale. Layout update later
> consumes that dependency at the smallest correct relayout boundary.
Do not start by changing all callers. First introduce storage and instrumentation
that behaves exactly like current `setLayoutDirty()`. Only after the baseline is
identical should callers start emitting more precise reasons.
### Core Data Model
Add a compact dirty-reason bitset. Prefer a plain integer-backed type over an
allocation-heavy structure.
Conceptual header-level API:
```cpp
enum class LayoutDirtyReason : Uint32 {
None = 0,
SelfLayout = 1u << 0,
NormalChildLayout = 1u << 1,
OutOfFlowChildLayout = 1u << 2,
IntrinsicSize = 1u << 3,
Style = 1u << 4,
FormattingContext = 1u << 5,
DocumentExtent = 1u << 6,
Viewport = 1u << 7,
};
using LayoutDirtyFlags = Uint32;
```
`UILayout` should own the local dirty state:
```cpp
class UILayout : public UIWidget {
protected:
bool mDirtyLayout{ false };
LayoutDirtyFlags mDirtyReasons{ 0 };
LayoutDirtyFlags mDeferredDirtyReasons{ 0 };
bool mUpdatingLayoutTree{ false };
};
```
The initial compatibility behavior should be:
```cpp
void UILayout::setLayoutDirty() {
setLayoutDirty( LayoutDirtyReason::SelfLayout );
}
```
Then add the explicit API:
```cpp
void UILayout::setLayoutDirty( LayoutDirtyReason reason );
void UILayout::addLayoutDirtyReasons( LayoutDirtyFlags reasons );
LayoutDirtyFlags UILayout::consumeLayoutDirtyReasons();
LayoutDirtyFlags UILayout::getLayoutDirtyReasons() const;
```
The reason bits must be merged, never replaced:
```cpp
mDirtyReasons |= toBits( reason );
```
If a layout is already dirty and receives a new reason, the dirty queue should
not add a duplicate node, but the node must retain the additional reason. This
is the first major difference from the current boolean-only state.
### Dirty Queue Entries
`UISceneNode::mDirtyLayouts` can remain a set of `UILayout*` for the first
implementation. Reason bits live on the `UILayout`, so the queue does not need
to allocate an entry object per invalidation.
Keep the existing reusable snapshot strategy:
- `mDirtyLayouts` remains the live coalescing set.
- `mDirtyLayoutsSnapshot` remains reusable `SmallVector<UILayout*, 64>`.
- `updateDirtyLayouts()` copies pointers into the snapshot, then clears the set.
- invalidations created during the pass stay in `mDirtyLayouts` for the next
outer invalidation-depth iteration.
Do not move `mDirtyLayouts` into a local temporary. Moving the set transfers its
buckets and makes large documents rebuild allocation state on the next wave.
The dirty queue can later evolve to:
```cpp
struct DirtyLayoutEntry {
UILayout* layout{ nullptr };
LayoutDirtyFlags reasons{ 0 };
};
```
but that should happen only if profiling shows per-layout reason reads are not
enough. The first version should avoid expanding queue memory and focus on
correct semantics.
### Coalescing Rules
The current queue performs two important coalescing operations:
1. If a dirty ancestor already exists and the path is all layouts, skip adding
the child.
2. If adding an ancestor, remove dirty descendants that the ancestor will update.
Typed reasons change when those rules are valid.
Safe ancestor coalescing:
- `SelfLayout` on a child can be coalesced into a dirty ancestor only if the
ancestor's update will recursively update that child before using stale child
geometry.
- `NormalChildLayout`, `FormattingContext`, and `IntrinsicSize` cannot be
blindly discarded when an ancestor is already dirty. Their reason bits must be
preserved somewhere the ancestor update can see.
- `OutOfFlowChildLayout` should coalesce toward the containing block, not the
normal parent chain.
- `DocumentExtent` should coalesce toward the document root, not ordinary UI
ancestors.
Initial conservative rule:
```cpp
if ( dirtyAncestorAlreadyQueued ) {
ancestor->addLayoutDirtyReasons( reasonsThatAncestorMustKnow );
node->addLayoutDirtyReasons( reasonsThatChildMustKeep );
return;
}
```
For phase 2, `reasonsThatAncestorMustKnow` can simply be all reasons. That keeps
behavior conservative without adding more queue entries. Later phases can split
reasons more precisely.
When adding an ancestor and removing dirty descendants, do not lose descendant
reason bits. Either:
- merge descendant reasons into the ancestor before erasing the descendant from
`mDirtyLayouts`, or
- leave descendants queued when their reasons are not implied by the ancestor.
The first option is simpler but can be too broad. The second option is more
browser-like, because child-dirty work can remain below a relayout boundary. The
recommended path is:
1. Phase 2: merge reasons upward to preserve correctness.
2. Phase 3+: stop merging reasons that belong to a child formatting context or
out-of-flow containing block.
### Update Pass Contract
`UILayout::updateLayoutTree()` should consume reasons in a controlled order.
Current behavior:
```cpp
mUpdatingLayoutTree = true;
updateLayout();
for ( auto layout : mLayouts )
layout->updateLayoutTree();
mUpdatingLayoutTree = false;
onLayoutUpdate();
```
This is parent-first. That is fine for many eepp layouts, but it is the source
of several HTML issues: a parent can measure child geometry before child layout
has discovered final intrinsic or block size.
Do not flip the whole tree to child-first. That would break layouters that must
assign child constraints before children can lay out. Instead, make the contract
explicit:
- parent layout establishes constraints,
- child layouts settle under those constraints,
- parent may perform a bounded reconciliation step if child exposed geometry
changed in a way the parent depends on.
Conceptual shape:
```cpp
void UILayout::updateLayoutTree() {
LayoutDirtyFlags reasons = consumeLayoutDirtyReasons();
mUpdatingLayoutTree = true;
LayoutUpdateResult before = captureExposedLayoutResult();
updateLayoutWithReasons( reasons );
for ( auto layout : mLayouts )
layout->updateLayoutTree();
LayoutDirtyFlags childEffects = consumeDeferredDirtyReasons();
if ( needsLocalReconcile( reasons, childEffects ) ) {
updateLayoutWithReasons( childEffects );
}
mUpdatingLayoutTree = false;
LayoutUpdateResult after = captureExposedLayoutResult();
propagateLayoutResult( before, after, reasons | childEffects );
onLayoutUpdate();
}
```
The first implementation does not need all these functions. It can start with:
- record dirty reasons,
- preserve deferred reasons during active layout,
- expose `onLayoutUpdate()` enough data to compare old/new size.
But this is the direction: absorbed invalidations must become explicit deferred
reasons, not vanish.
### Layout Update Result
Each layout pass should eventually report what changed externally.
Conceptual result:
```cpp
struct LayoutUpdateResult {
Sizef oldSize;
Sizef newSize;
Float oldMinIntrinsicWidth{ 0 };
Float newMinIntrinsicWidth{ 0 };
Float oldMaxIntrinsicWidth{ 0 };
Float newMaxIntrinsicWidth{ 0 };
bool positionChanged{ false };
bool childGeometryChanged{ false };
bool documentExtentChanged{ false };
};
```
Avoid computing intrinsic widths for every layout pass in phase 1. Intrinsic
fields should be lazy or provided only by layouters that already computed them.
The important minimal comparison is:
```cpp
bool exposedSizeChanged =
eeabs( oldSize.x - newSize.x ) > 0.01f ||
eeabs( oldSize.y - newSize.y ) > 0.01f;
```
If exposed size did not change, do not notify ancestors. If child positions
changed but the owner size did not, redraw/hit-testing may need invalidation,
but parent layout usually does not.
### Active Layout Guard
The current `UIRichText` guard is performance-critical:
- while the owner is already rebuilding/positioning its formatting stream,
descendant `LayoutAttributeChange` messages must not re-enter the owner,
- re-entry produces thousands of duplicate `rebuildRichText()` calls on large
Markdown documents.
The future guard should not simply `return 1`. It should record why it returned.
Conceptual API:
```cpp
void UILayout::deferLayoutDirtyReason( LayoutDirtyReason reason ) {
mDeferredDirtyReasons |= toBits( reason );
}
```
In `UIRichText::onMessage()`:
```cpp
if ( mUISceneNode->isUpdatingLayouts() && mUpdatingLayoutTree && !isOutOfFlow() ) {
invalidateIntrinsicSize();
deferLayoutDirtyReason( LayoutDirtyReason::FormattingContext );
return 1;
}
```
Do not use this as a generic "retry me every time" switch. A previous attempt to
dirty direct layout children from this guard fixed some stale cases but regressed
the README benchmark to thousands of extra rebuilds. The deferred reason must be
interpreted at the owner boundary and propagated only if the owner exposes a new
size/intrinsic result.
### Message Translation Layer
`NodeMessage::LayoutAttributeChange` is too coarse. Keep it for compatibility,
but translate it near the receiver into dirty reasons.
Initial mapping:
| Sender / condition | Receiver | Dirty reason |
|---|---|---|
| own size/padding/margin/style changed | self layout | `SelfLayout` |
| child normal-flow size changed | parent layout | `NormalChildLayout` |
| child text/font/style changed inside `UIRichText` | nearest rich-text owner | `FormattingContext | IntrinsicSize` |
| image natural size changed | image and rich-text owner | `IntrinsicSize | FormattingContext` |
| absolute child changed | containing block | `OutOfFlowChildLayout` |
| fixed child changed | viewport/document root | `OutOfFlowChildLayout | Viewport` |
| stylesheet combined in `UIWebView` | document root | `Style | DocumentExtent | Viewport` |
Do not try to infer all of this inside `UIWidget::notifyLayoutAttrChangeParent()`
at first. That function lacks enough CSS/layout context. Prefer receiver-side
translation in `UIRichText`, `UIHTMLTable`, block/flex/grid layouters, and
document root handling.
### Relayout Boundaries
A relayout boundary is a node where dirty work can usually stop because the
boundary owns a formatting context and can decide whether its exposed result
changed.
Boundaries to model:
- `UIRichText` formatting-context owner.
- `UIHTMLTable` / `TableLayouter`.
- flex and grid containers.
- out-of-flow containing blocks.
- `UIWebView` document root.
- future per-document `UISceneNode`.
Rules:
- Dirty descendants inside a boundary first dirty the boundary owner.
- Ancestors are notified only if the boundary's exposed size, intrinsic size, or
document extent changed.
- Boundaries with definite sizes can often avoid propagating size dirtiness while
still updating child geometry.
- Out-of-flow descendants must not dirty normal-flow auto size. They dirty their
containing block positioning context.
### Document Boundary Implementation
The recent deferred-CSS Hacker News issue showed a concrete current-architecture
gap:
- local file CSS can now load asynchronously through `<link defer>`,
- CSS can finish during `loadLayoutNodes()` while `mIsLoading` is still true,
- `combineStyleSheet()` then sets `mStyleDuringLoad` and returns,
- after loading finishes, styles are applied, but WebView document sizing was
not refreshed the way it is on viewport resize,
- `#bigbox > td` and `#bigbox > td > table` initially had stale heights and only
became correct after a 1px viewport-height change.
The bridge for the current architecture is:
- `UIWebView::refreshDocumentLayout()` owns WebView document refresh.
- It calls `containerUpdate()`.
- It calls `updateHTMLMinHeightForDocument()`.
- It dirties `webview::doc` if it is a layout.
- `UISceneNode::combineStyleSheet()` calls this after forced stylesheet reload.
- `UISceneNode::loadLayoutNodes()` calls it when `mStyleDuringLoad` was set.
This is not "perfect invalidation"; it is a scoped document-boundary repair.
The reason it belongs here, not in generic `UIRichText`, is that stylesheet
combines are document-level mutations. They can alter inherited metrics and
viewport/root/body constraints across the entire document. A generic RichText
ancestor retry fixes symptoms but reintroduces the Markdown rebuild storm.
Future typed invalidation should replace this bridge with:
```cpp
documentRoot->setLayoutDirty(
LayoutDirtyReason::Style |
LayoutDirtyReason::DocumentExtent |
LayoutDirtyReason::Viewport );
```
The document root would then refresh root/body/viewport state as part of
consuming those reasons.
### Intrinsic Size Cache Invalidation
Intrinsic caches exist in several places:
- `UIWidget::mIntrinsicWidthsDirty`,
- `UIRichText::mIntrinsicWidthsDirty`,
- `UILayouter::mIntrinsicWidthsDirty`,
- `TableLayouter` column min/max vectors,
- flex/grid item measurements.
Typed invalidation must make cache invalidation explicit.
Examples:
- `FormattingContext` implies `IntrinsicSize` for `UIRichText` only if text,
inline block, float, or wrapping-affecting style changed.
- `Style` implies `IntrinsicSize` when the property affects metrics:
`font-*`, `line-height`, `white-space`, `word-break`, `overflow-wrap`,
margins/padding/borders, width/min/max constraints, display, position, float,
table/flex/grid properties.
- `Style` does not imply `IntrinsicSize` for paint-only properties:
color, background color, text decoration when decoration does not affect
metrics, cursor, pointer-events.
The first implementation can conservatively invalidate intrinsic size for all
style changes in HTML content. Later optimize by property category. Correctness
comes first, but benchmark counters must catch over-broad invalidations.
### Property Categories
Add helper classification near the CSS/property layer:
```cpp
enum class LayoutPropertyImpact : Uint8 {
None,
PaintOnly,
SelfGeometry,
ChildGeometry,
IntrinsicSize,
FormattingContext,
Document,
};
```
Examples:
| Property | Impact |
|---|---|
| `color` | `PaintOnly` |
| `background-color` | `PaintOnly` |
| `font-size` | `FormattingContext | IntrinsicSize` |
| `line-height` | `FormattingContext | IntrinsicSize` |
| `white-space` | `FormattingContext | IntrinsicSize` |
| `width` / `height` | `SelfGeometry | IntrinsicSize` |
| `min-width` / `max-width` | `SelfGeometry | IntrinsicSize` |
| `display` | `ChildGeometry | FormattingContext | IntrinsicSize` |
| `position` | `SelfGeometry | OutOfFlowChildLayout` depending on value |
| `float` / `clear` | `FormattingContext | IntrinsicSize` |
| table layout properties | `IntrinsicSize | ChildGeometry` |
| flex/grid properties | `ChildGeometry | IntrinsicSize` |
This allows style reload to emit reasoned invalidation instead of every setter
calling the same coarse message path.
### Parent Notification Policy
Ancestor notification should happen after an owner has reconciled its own
layout, not before.
Current dangerous pattern:
```cpp
childChanged();
notifyLayoutAttrChangeParent();
tryUpdateLayout();
```
This lets a generic ancestor become the queued dirty layout and measure stale
child geometry before the actual owner has recomputed.
Preferred pattern:
```cpp
markOwnerDirty( reason );
owner recomputes;
if ( owner exposed result changed )
notify dependent ancestor;
```
For existing code, migrate class by class:
1. `UIRichText`: do not relay descendant changes before owner reflow.
2. `UIHTMLTable`: invalidate table intrinsic widths locally; notify ancestors
only when table exposed size/intrinsic result changes.
3. `FlexLayouter` / `GridLayouter`: keep item collection local; notify parent
only when container size/intrinsic result changes.
4. Generic `UILinearLayout` / `UIGridLayout` remain simpler; they are not HTML
formatting contexts unless explicitly used as document wrappers.
### Out-Of-Flow Handling
Out-of-flow positioning needs separate dirty routing.
Rules:
- `position: absolute` dirties the nearest containing block.
- `position: fixed` dirties the viewport/document root.
- out-of-flow size/position does not contribute to normal-flow parent auto size,
except for current compatibility code that intentionally includes some
absolute descendants in body effective content height.
- `UIRichText::rebuildRichText()` should continue skipping out-of-flow children.
- `updateOutOfFlowPosition()` should run after normal-flow layout because it
depends on containing block geometry.
Typed reason:
```cpp
containingBlock->setLayoutDirty( LayoutDirtyReason::OutOfFlowChildLayout );
```
Do not let this become `NormalChildLayout`, or fixed/absolute elements will
reintroduce unnecessary parent auto-size recomputation.
### Tables
Table layout needs a dedicated result object because table dependencies are
multi-stage:
1. collect rows/cells,
2. compute intrinsic column widths,
3. resolve table used width,
4. assign cell widths,
5. lay out cell contents,
6. compute row heights,
7. set table wrapper size.
`TableLayouter` should eventually expose:
```cpp
struct TableLayoutResult {
bool rowHeightsChanged{ false };
bool columnWidthsChanged{ false };
bool minIntrinsicWidthChanged{ false };
bool maxIntrinsicWidthChanged{ false };
bool tableSizeChanged{ false };
};
```
When a cell changes:
- dirty table intrinsic widths if the change can affect min/max width,
- dirty row heights if the cell block-size can change,
- do not notify the table parent until the table pass knows whether
`tableSizeChanged` or intrinsic widths changed.
Important: a previous attempt to schedule generic table retries during packing
did not fix the deferred-CSS issue and was not the right abstraction. Tables
need result-based propagation, not blind retry.
### RichText
`UIRichText` should grow a small formatting dirty state:
```cpp
bool mFormattingContextDirty{ false };
bool mIntrinsicSizeDirty{ true };
LayoutDirtyFlags mDeferredFormattingReasons{ 0 };
```
The update path should be:
1. collect old exposed size and maybe cached intrinsic generation,
2. rebuild rich text only if formatting context is dirty or layout constraints
changed,
3. update `Graphics::RichText` layout,
4. position child widgets/text nodes,
5. compare exposed size,
6. notify parent only if exposed size/intrinsic result changed.
Avoid rebuilding the rich-text stream for:
- pure child position updates,
- paint-only style changes,
- fixed-position descendant changes,
- descendant notifications already produced by this same active positioning
pass.
Potential generation counters:
```cpp
Uint32 mFormattingGeneration{ 0 };
Uint32 mLastLaidOutFormattingGeneration{ 0 };
Uint32 mConstraintGeneration{ 0 };
```
These can prevent repeated rebuilds when the dirty queue processes multiple
layout roots that point at the same RichText owner.
### WebView Document Root
Current architecture:
- `UIWebView` is a scroll view in the normal UI scene.
- `mDocContainer` is a `UILinearLayout`.
- `html` and `body` are children inside that container.
- root/body/document sizing is maintained partly by `UIWebView`.
Near-term:
- keep WebView-specific document refresh in `UIWebView`,
- route async stylesheet load and resource completion through document refresh
instead of generic ancestor invalidation,
- keep the helper scoped to `UI_TYPE_WEBVIEW` and `UI_TYPE_HTML_HTML`.
Future per-document scene:
- each `UIWebView` owns a document `UISceneNode`,
- the document scene has its own dirty style/layout queues,
- root/body/viewport handling is a document-scene responsibility,
- host UI receives only final scrollable size / viewport invalidations,
- standalone `UIRichText` continues to use shared formatting and layouters but
does not inherit WebView document semantics.
### Rollback Checkpoint Implementation
At the end of every phase:
```sh
git stash push -m "phase-N topic validated-tests" -- <changed files>
git stash apply stash@{0}
```
Do not use `git stash pop`; the checkpoint must remain. If a phase spans many
files, include all relevant files explicitly. If generated files or unrelated
dirty user files exist, do not stash them unless they belong to the phase.
Validation should be mentioned in the stash message:
```text
phase-3 richtext-formatting-dirty validated-markdown-hn-image
phase-5 webview-document-boundary validated-deferred-css-markdown
```
Before creating the stash:
- run focused tests,
- run `Benchmark.MarkdownReadme`,
- run `git diff --check`,
- inspect `git status --short` and make sure unrelated files are not included.
## Propagation Rules
### Self Layout
+2
View File
@@ -74,6 +74,8 @@ class EE_API UIWebView : public UIScrollView {
void setDefaultTimeout( const Time& timeout );
void refreshDocumentLayout();
Uint32 onNavigationStarted( std::function<void( const URI& )> cb );
Uint32 onNavigationCompleted( std::function<void( const URI& )> cb );
+32 -1
View File
@@ -23,6 +23,7 @@
#include <eepp/ui/uistyle.hpp>
#include <eepp/ui/uithememanager.hpp>
#include <eepp/ui/uitooltip.hpp>
#include <eepp/ui/uiwebview.hpp>
#include <eepp/ui/uiwidgetcreator.hpp>
#include <eepp/ui/uiwindow.hpp>
#include <eepp/window/engine.hpp>
@@ -35,6 +36,30 @@ using namespace EE::Network;
namespace EE { namespace UI {
static void refreshWebViewDocumentLayoutAfterStyleChange( UIWidget* root ) {
if ( !root )
return;
// A stylesheet combine is a document-level style mutation: inherited font metrics,
// table sizing, percentage constraints, and root/body dimensions can all change after the
// first HTML layout pass. UIWebView already performs this html/body reset when the viewport
// size changes; deferred CSS needs the same document-boundary invalidation even if the
// viewport did not move. Keeping it scoped to WebView documents avoids reopening the generic
// RichText parent-recompute storm, while the final html dirty mark gives the normal layout
// queue one coalesced pass from the document root.
auto webViews = root->findAllByType( UI_TYPE_WEBVIEW );
for ( auto webViewNode : webViews ) {
auto* webView = webViewNode->asType<UIWebView>();
webView->refreshDocumentLayout();
}
auto htmls = root->findAllByType( UI_TYPE_HTML_HTML );
for ( auto html : htmls ) {
if ( html->isLayout() )
html->asType<UILayout>()->setLayoutDirty();
}
}
UISceneNode* UISceneNode::New( EE::Window::Window* window ) {
return eeNew( UISceneNode, ( window ) );
}
@@ -378,7 +403,8 @@ UIWidget* UISceneNode::loadLayoutNodes( pugi::xml_node node, Node* parent, const
innerClock.getElapsedTimeAndReset().asMilliseconds() );
}
if ( mStyleDuringLoad ) {
bool styleChangedDuringLoad = mStyleDuringLoad;
if ( styleChangedDuringLoad ) {
updateStyleSheet( false );
mStyleDuringLoad = false;
}
@@ -386,6 +412,9 @@ UIWidget* UISceneNode::loadLayoutNodes( pugi::xml_node node, Node* parent, const
for ( auto& widget : widgets )
widget->reloadStyle( true, true, true );
if ( styleChangedDuringLoad )
refreshWebViewDocumentLayoutAfterStyleChange( mRoot );
if ( mVerbose ) {
Log::debug( "UISceneNode::loadLayoutNodes reloaded styles in: %.2f ms",
innerClock.getElapsedTimeAndReset().asMilliseconds() );
@@ -456,6 +485,8 @@ void UISceneNode::combineStyleSheet( const CSS::StyleSheet& styleSheet, bool for
}
updateStyleSheet( forceReloadStyle );
if ( forceReloadStyle )
refreshWebViewDocumentLayoutAfterStyleChange( mRoot );
}
void UISceneNode::combineStyleSheet( const std::string& inlineStyleSheet, bool forceReloadStyle,
+7
View File
@@ -274,6 +274,13 @@ void UIWebView::setDefaultTimeout( const Time& timeout ) {
mDefaultTimeout = timeout;
}
void UIWebView::refreshDocumentLayout() {
containerUpdate();
updateHTMLMinHeightForDocument();
if ( mDocContainer && mDocContainer->isLayout() )
mDocContainer->asType<UILayout>()->setLayoutDirty();
}
void UIWebView::navigateToHistoryIndex( int index ) {
mHistoryIndex = index;
loadURI( mHistory[mHistoryIndex], true );
+59
View File
@@ -4106,6 +4106,65 @@ UTEST( UIHTML, HtmlContainsTableBodyHeight ) {
Engine::destroySingleton();
}
UTEST( UIHTML, DeferredCSSKeepsTableHeightStableAfterViewportResize ) {
auto win = Engine::instance()->createWindow(
WindowSettings( 1024, 768, "deferred css table height stable", WindowStyle::Default,
WindowBackend::Default, 32, {}, 1, false, true ),
ContextSettings( false, 0, 0, GLv_default, true, false ) );
UISceneNode* sceneNode = init_test_inline_block();
sceneNode->setURI( "file://" + Sys::getProcessPath() + "assets/html/" );
UIWebView* webView = UIWebView::New();
webView->setParent( sceneNode->getRoot() );
webView->setPixelsSize( win->getWidth(), win->getHeight() );
webView->setLayoutSizePolicy( SizePolicy::Fixed, SizePolicy::Fixed );
std::string html;
ASSERT_TRUE( FileSystem::fileGet( "assets/html/hn_empty_thread.html", html ) );
sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( html ),
webView->getDocumentContainer(),
String::hash( "hn-empty-thread-deferred-css" ) );
auto* titleCell = sceneNode->getRoot()->querySelector( ".title" );
ASSERT_TRUE( titleCell != nullptr );
ASSERT_TRUE( titleCell->isType( UI_TYPE_RICHTEXT ) );
const Color expectedTitleColor( "#828282" );
for ( int i = 0;
i < 120 && titleCell->asType<UIRichText>()->getFontColor() != expectedTitleColor; ++i ) {
win->getInput()->update();
SceneManager::instance()->update();
sceneNode->updateDirtyLayouts();
Sys::sleep( Milliseconds( 5 ) );
}
ASSERT_TRUE( titleCell->asType<UIRichText>()->getFontColor() == expectedTitleColor );
win->getInput()->update();
SceneManager::instance()->update();
sceneNode->updateDirtyLayouts();
auto* bigboxTd = sceneNode->getRoot()->querySelector( "#bigbox > td" );
auto* bigboxTable = sceneNode->getRoot()->querySelector( "#bigbox > td > table" );
ASSERT_TRUE( bigboxTd != nullptr );
ASSERT_TRUE( bigboxTable != nullptr );
const Float initialTdHeight = bigboxTd->getPixelsSize().getHeight();
const Float initialTableHeight = bigboxTable->getPixelsSize().getHeight();
EXPECT_GT( initialTdHeight, 0.f );
EXPECT_GT( initialTableHeight, 0.f );
webView->setPixelsSize( win->getWidth(), win->getHeight() + 1 );
win->getInput()->update();
SceneManager::instance()->update();
sceneNode->updateDirtyLayouts();
EXPECT_NEAR( initialTdHeight, bigboxTd->getPixelsSize().getHeight(), 0.1f );
EXPECT_NEAR( initialTableHeight, bigboxTable->getPixelsSize().getHeight(), 0.1f );
Engine::destroySingleton();
}
UTEST( UIHTML, ImageCSSWidthOverridesHTMLWidthAttribute ) {
auto win = Engine::instance()->createWindow(
WindowSettings( 1024, 768, "img css width overrides html width attr", WindowStyle::Default,