Add CSS paint ordering and stacking contexts to HTML widgets

Implement spec-oriented paint phases for normal flow, floats, and
positioned descendants, including atomic stacking groups and applicable
z-index handling for flex and grid items.

Share cached ordering between drawing and hit testing, propagate
invalidation across stacking scopes, and preserve ancestor transforms
and clipping for promoted descendants.

Add regression coverage for paint order, hit testing, reparenting,
clipping, flex ordering, and the old Reddit layout. Document the
supported stacking model and remove the completed implementation plan.
This commit is contained in:
Martín Lucas Golini
2026-08-14 01:43:34 -03:00
parent ce5c36c11d
commit cc87ed6924
7 changed files with 751 additions and 1611 deletions
+337 -62
View File
@@ -3,6 +3,7 @@
#include <eepp/ui/css/stylesheetlength.hpp>
#include <eepp/ui/flexlayouter.hpp>
#include <eepp/ui/gridlayouter.hpp>
#include <eepp/ui/uiborderdrawable.hpp>
#include <eepp/ui/uihtmlwidget.hpp>
#include <eepp/ui/uilayouter.hpp>
#include <eepp/ui/uilayoutermanager.hpp>
@@ -162,6 +163,8 @@ UIHTMLWidget::~UIHTMLWidget() {
eeSAFE_DELETE( mLayouter );
eeSAFE_DELETE( mFlexState );
eeSAFE_DELETE( mGridState );
eeSAFE_DELETE( mPaintOrderCache );
eeSAFE_DELETE( mHTMLPaintAncestors );
}
UILayouter* UIHTMLWidget::getLayouter() {
@@ -223,6 +226,7 @@ void UIHTMLWidget::setDisplay( CSSDisplay display ) {
}
onDisplayChange();
updatePaintOrderFlag();
}
}
@@ -351,6 +355,8 @@ void UIHTMLWidget::setVisibility( CSSVisibility val ) {
void UIHTMLWidget::setCSSPosition( CSSPosition position ) {
if ( mPosition != position ) {
mPosition = position;
if ( Node* parent = getParent(); parent && parent->isType( UI_TYPE_HTML_WIDGET ) )
parent->asType<UIHTMLWidget>()->updatePaintOrderFlag();
if ( position == CSSPosition::Absolute || position == CSSPosition::Fixed ) {
// Out-of-flow elements should not stretch to their containing block
// until updateOutOfFlowPosition() computes the correct size from CSS
@@ -369,6 +375,8 @@ void UIHTMLWidget::setCSSPosition( CSSPosition position ) {
void UIHTMLWidget::setCSSFloat( CSSFloat cssFloat ) {
if ( mFloat != cssFloat ) {
mFloat = cssFloat;
if ( Node* parent = getParent(); parent && parent->isType( UI_TYPE_HTML_WIDGET ) )
parent->asType<UIHTMLWidget>()->updatePaintOrderFlag();
// A width:auto block normally fills its containing block, while a float uses the CSS
// shrink-to-fit width. Represent that used-width distinction with WrapContent so the
// floated box's own layouter cannot stretch it back after its parent measured it.
@@ -494,103 +502,354 @@ void UIHTMLWidget::setOffsets( const Rectf& offsets ) {
}
void UIHTMLWidget::setZIndex( int zIndex ) {
mZIndex = zIndex;
const CSSZIndex value{ zIndex, false };
if ( mZIndex == value )
return;
mZIndex = value;
Node* p = getParent();
if ( p && p->isType( UI_TYPE_HTML_WIDGET ) )
p->asType<UIHTMLWidget>()->updateZIndexSortFlag();
p->asType<UIHTMLWidget>()->updatePaintOrderFlag();
}
void UIHTMLWidget::setZIndexAuto() {
if ( mZIndex.isAuto )
return;
mZIndex = {};
Node* p = getParent();
if ( p && p->isType( UI_TYPE_HTML_WIDGET ) )
p->asType<UIHTMLWidget>()->updatePaintOrderFlag();
}
bool UIHTMLWidget::hasApplicableZIndex() const {
if ( mZIndex.isAuto )
return false;
if ( isCSSPositioned() )
return true;
Node* parent = getParent();
return parent && parent->isType( UI_TYPE_HTML_WIDGET ) &&
( parent->asType<UIHTMLWidget>()->isFlex() || parent->asType<UIHTMLWidget>()->isGrid() );
}
bool UIHTMLWidget::createsSupportedStackingGroup() const {
return mPosition == CSSPosition::Fixed || mPosition == CSSPosition::Sticky ||
hasApplicableZIndex();
}
void UIHTMLWidget::setNeedsOrderSort( bool val ) {
mNeedsOrderSort = val;
if ( mNeedsOrderSort != val ) {
mNeedsOrderSort = val;
invalidatePaintOrder();
}
}
void UIHTMLWidget::updateZIndexSortFlag() {
bool needs = false;
for ( Node* child = getFirstChild(); child; child = child->getNextNode() ) {
if ( child->isType( UI_TYPE_HTML_WIDGET ) &&
child->asType<UIHTMLWidget>()->getZIndex() != 0 ) {
needs = true;
break;
void UIHTMLWidget::invalidatePaintOrder() {
for ( Node* node = this; node; node = node->getParent() ) {
if ( node->isType( UI_TYPE_HTML_WIDGET ) ) {
auto* widget = node->asType<UIHTMLWidget>();
if ( widget->mPaintOrderCache )
widget->mPaintOrderCache->dirty = true;
}
}
mNeedsZIndexSort = needs;
}
void UIHTMLWidget::updatePaintOrderFlag() {
bool needsPaintOrder = false;
bool hasStackingDescendant = false;
for ( Node* child = getFirstChild(); child; child = child->getNextNode() ) {
if ( !child->isType( UI_TYPE_HTML_WIDGET ) )
continue;
auto* htmlChild = child->asType<UIHTMLWidget>();
needsPaintOrder |=
( !isFlex() && !isGrid() && htmlChild->getCSSFloat() != CSSFloat::None ) ||
htmlChild->getCSSPosition() != CSSPosition::Static || htmlChild->hasApplicableZIndex();
hasStackingDescendant |= htmlChild->isCSSPositioned() ||
htmlChild->createsSupportedStackingGroup() ||
htmlChild->mHasStackingDescendant;
}
const bool stackingChanged = mHasStackingDescendant != hasStackingDescendant;
mNeedsPaintOrder = needsPaintOrder;
mHasStackingDescendant = hasStackingDescendant;
invalidatePaintOrder();
if ( stackingChanged ) {
Node* parent = getParent();
if ( parent && parent->isType( UI_TYPE_HTML_WIDGET ) )
parent->asType<UIHTMLWidget>()->updatePaintOrderFlag();
}
}
void UIHTMLWidget::onChildCountChange( Node* child, const bool& removed ) {
UILayout::onChildCountChange( child, removed );
if ( !removed )
updateZIndexSortFlag();
else if ( child->isType( UI_TYPE_HTML_WIDGET ) &&
child->asType<UIHTMLWidget>()->getZIndex() != 0 )
updateZIndexSortFlag();
updatePaintOrderFlag();
}
void UIHTMLWidget::buildDrawOrderVector( SmallVector<Node*, 127>& out ) const {
bool flexSort = false;
bool directionReverse = false;
const auto& paintOrder = getPaintOrder();
out.insert( out.end(), paintOrder.begin(), paintOrder.end() );
}
if ( isFlex() ) {
CSSFlexDirection dir = getFlexDirection();
directionReverse =
dir == CSSFlexDirection::RowReverse || dir == CSSFlexDirection::ColumnReverse;
flexSort = mNeedsOrderSort || directionReverse;
static HTMLPaintCategory getPaintCategory( Node* node ) {
// This is the supported CSS 2 Appendix E subset. Inline fragments remain owned by RichText, so
// their background/text sub-phases cannot be split here without fragment-level paint records.
if ( !node->isType( UI_TYPE_HTML_WIDGET ) )
return HTMLPaintCategory::NormalFlow;
const auto* widget = node->asType<UIHTMLWidget>();
const bool positioned = widget->isCSSPositioned();
const bool applicableZIndex = widget->hasApplicableZIndex();
Node* parent = widget->getParent();
const bool flexOrGridItem =
parent && parent->isType( UI_TYPE_HTML_WIDGET ) &&
( parent->asType<UIHTMLWidget>()->isFlex() || parent->asType<UIHTMLWidget>()->isGrid() );
if ( applicableZIndex && widget->getZIndex() < 0 )
return HTMLPaintCategory::NegativePositioned;
if ( !positioned && !applicableZIndex && !flexOrGridItem &&
widget->getCSSFloat() != CSSFloat::None )
return HTMLPaintCategory::Float;
if ( applicableZIndex && widget->getZIndex() > 0 )
return HTMLPaintCategory::PositivePositioned;
if ( positioned || applicableZIndex )
return HTMLPaintCategory::PositionedAutoOrZero;
return HTMLPaintCategory::NormalFlow;
}
bool UIHTMLWidget::isHTMLStackingScope() const {
if ( createsSupportedStackingGroup() )
return true;
for ( Node* parent = getParent(); parent; parent = parent->getParent() ) {
if ( parent->isType( UI_TYPE_HTML_WIDGET ) )
return false;
}
return true;
}
void UIHTMLWidget::buildCSSChildOrder( SmallVector<Node*, 16>& out ) const {
for ( Node* child = getFirstChild(); child; child = child->getNextNode() )
out.push_back( child );
if ( flexSort && mNeedsOrderSort ) {
if ( !isFlex() && !isGrid() )
return;
if ( mNeedsOrderSort ) {
std::stable_sort( out.begin(), out.end(), []( Node* a, Node* b ) {
int aOrder = ( a->isWidget() && a->isType( UI_TYPE_HTML_WIDGET ) )
? a->asType<UIHTMLWidget>()->getOrder()
: 0;
int bOrder = ( b->isWidget() && b->isType( UI_TYPE_HTML_WIDGET ) )
? b->asType<UIHTMLWidget>()->getOrder()
: 0;
const int aOrder =
a->isType( UI_TYPE_HTML_WIDGET ) ? a->asType<UIHTMLWidget>()->getOrder() : 0;
const int bOrder =
b->isType( UI_TYPE_HTML_WIDGET ) ? b->asType<UIHTMLWidget>()->getOrder() : 0;
return aOrder < bOrder;
} );
}
if ( flexSort && directionReverse )
std::reverse( out.begin(), out.end() );
if ( mNeedsZIndexSort ) {
std::stable_sort( out.begin(), out.end(), []( Node* a, Node* b ) {
int aZ =
( a->isType( UI_TYPE_HTML_WIDGET ) ) ? a->asType<UIHTMLWidget>()->getZIndex() : 0;
int bZ =
( b->isType( UI_TYPE_HTML_WIDGET ) ) ? b->asType<UIHTMLWidget>()->getZIndex() : 0;
return aZ < bZ;
} );
}
}
void UIHTMLWidget::drawChildren() {
bool needsSort = mNeedsOrderSort || mNeedsZIndexSort;
void UIHTMLWidget::resetPromotedPaintState() const {
for ( Node* child = getFirstChild(); child; child = child->getNextNode() ) {
if ( !child->isType( UI_TYPE_HTML_WIDGET ) )
continue;
auto* widget = child->asType<UIHTMLWidget>();
widget->mHTMLPaintOwner = nullptr;
if ( widget->mHTMLPaintAncestors )
widget->mHTMLPaintAncestors->clear();
widget->mHasPromotedChild = false;
if ( widget->mPaintOrderCache )
widget->mPaintOrderCache->dirty = true;
widget->resetPromotedPaintState();
}
}
if ( isFlex() ) {
CSSFlexDirection dir = getFlexDirection();
if ( dir == CSSFlexDirection::RowReverse || dir == CSSFlexDirection::ColumnReverse )
needsSort = true;
void UIHTMLWidget::promoteHTMLPaintNode( UIHTMLWidget* widget ) const {
mHasActivePromotions = true;
widget->mHTMLPaintOwner = this;
if ( !widget->mHTMLPaintAncestors )
widget->mHTMLPaintAncestors = eeNew( UIHTMLPaintAncestorVector, () );
for ( Node* parent = widget->getParent(); parent && parent != this;
parent = parent->getParent() ) {
if ( parent->isType( UI_TYPE_HTML_WIDGET ) )
widget->mHTMLPaintAncestors->push_back( parent->asType<UIHTMLWidget>() );
}
if ( widget->getParent()->isType( UI_TYPE_HTML_WIDGET ) )
widget->getParent()->asType<UIHTMLWidget>()->mHasPromotedChild = true;
}
void UIHTMLWidget::collectStackingScopeItems( UIHTMLWidget* container, SmallVector<Node*, 16>& out,
bool directChildren ) const {
if ( container->isFlex() || container->isGrid() ) {
SmallVector<Node*, 16> children;
container->buildCSSChildOrder( children );
for ( Node* child : children )
collectStackingScopeChild( child, out, directChildren );
return;
}
for ( Node* child = container->getFirstChild(); child; child = child->getNextNode() )
collectStackingScopeChild( child, out, directChildren );
}
void UIHTMLWidget::collectStackingScopeChild( Node* child, SmallVector<Node*, 16>& out,
bool directChildren ) const {
if ( directChildren )
out.push_back( child );
if ( !child->isType( UI_TYPE_HTML_WIDGET ) )
return;
auto* widget = child->asType<UIHTMLWidget>();
if ( widget->createsSupportedStackingGroup() ) {
if ( !directChildren ) {
out.push_back( widget );
promoteHTMLPaintNode( widget );
}
return;
}
if ( !directChildren && widget->isCSSPositioned() ) {
out.push_back( widget );
promoteHTMLPaintNode( widget );
}
collectStackingScopeItems( widget, out, false );
}
const SmallVector<Node*, 16>& UIHTMLWidget::getPaintOrder() const {
if ( !mPaintOrderCache )
mPaintOrderCache = eeNew( UIHTMLPaintOrderCache, () );
if ( !mPaintOrderCache->dirty )
return mPaintOrderCache->items;
auto& out = mPaintOrderCache->items;
out.clear();
const bool collectStacking = isHTMLStackingScope() && mHasStackingDescendant;
// A scope must clear its previous promotion metadata even when the last promoted descendant
// just stopped qualifying; otherwise its old parent can keep skipping the node indefinitely.
if ( mHasActivePromotions ) {
resetPromotedPaintState();
mHasActivePromotions = false;
}
if ( collectStacking ) {
collectStackingScopeItems( const_cast<UIHTMLWidget*>( this ), out, true );
} else {
buildCSSChildOrder( out );
}
if ( !needsSort ) {
if ( mNeedsPaintOrder || collectStacking ) {
std::stable_sort( out.begin(), out.end(), []( Node* a, Node* b ) {
const auto aCategory = getPaintCategory( a );
const auto bCategory = getPaintCategory( b );
if ( aCategory != bCategory )
return aCategory < bCategory;
if ( aCategory == HTMLPaintCategory::NegativePositioned ||
aCategory == HTMLPaintCategory::PositivePositioned )
return a->asType<UIHTMLWidget>()->getZIndex() <
b->asType<UIHTMLWidget>()->getZIndex();
return false;
} );
}
mPaintOrderCache->dirty = false;
++mPaintOrderCache->rebuildCount;
return out;
}
Uint32 UIHTMLWidget::getPaintOrderRebuildCount() const {
return mPaintOrderCache ? mPaintOrderCache->rebuildCount : 0;
}
std::vector<Node*> UIHTMLWidget::debugGetHTMLPaintOrder() const {
const auto& order = getPaintOrder();
return { order.begin(), order.end() };
}
std::vector<Node*> UIHTMLWidget::debugGetHTMLHitTestOrder() const {
const auto& order = getPaintOrder();
return { order.rbegin(), order.rend() };
}
void UIHTMLWidget::drawHTMLPaintNode( Node* node ) {
if ( !node->isType( UI_TYPE_HTML_WIDGET ) ||
node->asType<UIHTMLWidget>()->mHTMLPaintOwner != this ) {
node->nodeDraw();
return;
}
auto* promoted = node->asType<UIHTMLWidget>();
eeASSERT( promoted->mHTMLPaintAncestors != nullptr );
const auto& ancestors = *promoted->mHTMLPaintAncestors;
for ( auto it = ancestors.rbegin(); it != ancestors.rend(); ++it ) {
auto* ancestor = *it;
ancestor->matrixSet();
ancestor->smartClipStart( ancestor->getClipType(),
ancestor->isMeOrParentTreeScaledOrRotatedOrFrameBuffer() );
}
node->nodeDraw();
for ( auto* ancestor : ancestors ) {
ancestor->smartClipEnd( ancestor->getClipType(),
ancestor->isMeOrParentTreeScaledOrRotatedOrFrameBuffer() );
ancestor->matrixUnset();
}
}
bool UIHTMLWidget::containsHTMLPaintClipPoint( const Vector2f& point ) const {
if ( !isClipped() )
return true;
const Vector2f localPoint = convertToNodeSpace( point );
Rectf clipRect( Vector2f::Zero, getPixelsSize() );
switch ( getClipType() ) {
case ClipType::PaddingBox: {
const Rectf& padding = getPixelsPadding();
clipRect = Rectf( padding.Left, padding.Top,
getPixelsSize().getWidth() - padding.Left - padding.Right,
getPixelsSize().getHeight() - padding.Top - padding.Bottom );
break;
}
case ClipType::BorderBox: {
if ( mBorder ) {
const Rectf borderDiff = mBorder->getBorderBoxDiff();
clipRect = Rectf( borderDiff.Left, borderDiff.Top,
getPixelsSize().getWidth() + borderDiff.Right,
getPixelsSize().getHeight() + borderDiff.Bottom );
}
break;
}
case ClipType::ContentBox:
case ClipType::None:
break;
}
return clipRect.contains( localPoint );
}
bool UIHTMLWidget::canHitHTMLPaintNode( Node* node, const Vector2f& point ) const {
if ( !node->isType( UI_TYPE_HTML_WIDGET ) )
return true;
const auto* promoted = node->asType<UIHTMLWidget>();
if ( promoted->mHTMLPaintOwner != this || !promoted->mHTMLPaintAncestors )
return true;
for ( auto* ancestor : *promoted->mHTMLPaintAncestors ) {
if ( !ancestor->containsHTMLPaintClipPoint( point ) )
return false;
}
return true;
}
bool UIHTMLWidget::needsHTMLPaintTraversal() const {
if ( mNeedsOrderSort || mNeedsPaintOrder || mHasPromotedChild ||
( isHTMLStackingScope() && mHasStackingDescendant ) )
return true;
return false;
}
bool UIHTMLWidget::shouldSkipHTMLPaintNode( Node* node ) const {
return node->isType( UI_TYPE_HTML_WIDGET ) &&
node->asType<UIHTMLWidget>()->mHTMLPaintOwner != nullptr &&
node->asType<UIHTMLWidget>()->mHTMLPaintOwner != this;
}
void UIHTMLWidget::drawChildren() {
if ( !needsHTMLPaintTraversal() ) {
UILayout::drawChildren();
return;
}
SmallVector<Node*, 127> sortedChildren;
buildDrawOrderVector( sortedChildren );
for ( auto* child : sortedChildren ) {
for ( auto* child : getPaintOrder() ) {
if ( shouldSkipHTMLPaintNode( child ) )
continue;
if ( child->isVisible() )
child->nodeDraw();
drawHTMLPaintNode( child );
}
}
Node* UIHTMLWidget::overFind( const Vector2f& point ) {
if ( !mNeedsOrderSort && !mNeedsZIndexSort )
if ( !needsHTMLPaintTraversal() )
return UILayout::overFind( point );
Node* pOver = nullptr;
@@ -602,11 +861,14 @@ Node* UIHTMLWidget::overFind( const Vector2f& point ) {
writeNodeFlag( NODE_FLAG_MOUSEOVER_ME_OR_CHILD, 1 );
mSceneNode->addMouseOverNode( this );
SmallVector<Node*, 127> sortedChildren;
buildDrawOrderVector( sortedChildren );
const auto& sortedChildren = getPaintOrder();
// Iterate last-to-first: highest z-index = topmost = hit first
// Drawing and hit-testing share one sequence; reverse it so the last painted node wins.
for ( auto it = sortedChildren.rbegin(); it != sortedChildren.rend(); ++it ) {
if ( shouldSkipHTMLPaintNode( *it ) )
continue;
if ( !canHitHTMLPaintNode( *it, point ) )
continue;
Node* childOver = ( *it )->overFind( point );
if ( childOver ) {
pOver = childOver;
@@ -698,6 +960,8 @@ void UIHTMLWidget::setOrder( int val ) {
auto* fs = ensureFlexState();
if ( fs->order != val ) {
fs->order = val;
if ( Node* parent = getParent(); parent && parent->isType( UI_TYPE_HTML_WIDGET ) )
parent->asType<UIHTMLWidget>()->invalidatePaintOrder();
notifyLayoutAttrChange( LayoutInvalidation::ContainerLayout );
}
}
@@ -899,7 +1163,7 @@ std::string UIHTMLWidget::getPropertyString( const PropertyDefinition* propertyD
case PropertyId::Left:
return mLeftEq;
case PropertyId::ZIndex:
return String::toString( mZIndex );
return mZIndex.isAuto ? "auto" : String::toString( mZIndex.value );
case PropertyId::AlignmentBaseline:
return std::string( CSSBaselineAlignmentHelper::toString( mBaselineAlign ) );
case PropertyId::FlexDirection:
@@ -1043,7 +1307,10 @@ bool UIHTMLWidget::applyProperty( const StyleSheetProperty& attribute ) {
return applied;
}
case PropertyId::ZIndex: {
setZIndex( attribute.asInt() );
if ( String::trim( attribute.asString() ) == "auto" )
setZIndexAuto();
else
setZIndex( attribute.asInt() );
return true;
}
case PropertyId::Top: {
@@ -1523,6 +1790,14 @@ void UIHTMLWidget::updateScrollListeners() {
void UIHTMLWidget::onParentChange() {
UILayout::onParentChange();
// Promotion is derived from ancestry. Reparenting can detach this subtree before its former
// stacking scope gets a chance to rebuild, so clear all non-owning scope metadata here.
mHTMLPaintOwner = nullptr;
if ( mHTMLPaintAncestors )
mHTMLPaintAncestors->clear();
mHasPromotedChild = false;
resetPromotedPaintState();
invalidatePaintOrder();
updateScrollListeners();
}
+51 -5
View File
@@ -2258,7 +2258,7 @@ UTEST( FlexContainer, orderPaintSortNegatives ) {
Engine::destroySingleton();
}
UTEST( FlexContainer, directionReversePaintColumnReverse ) {
UTEST( FlexContainer, directionReverseLayoutKeepsColumnPaintOrder ) {
Engine::instance()->createWindow( WindowSettings( 1024, 650, "Flex Test", WindowStyle::Default,
WindowBackend::Default, 32, {}, 1, false,
true ),
@@ -2294,13 +2294,20 @@ UTEST( FlexContainer, directionReversePaintColumnReverse ) {
EXPECT_GT( child1->getPixelsPosition().y, child2->getPixelsPosition().y );
EXPECT_GT( child2->getPixelsPosition().y, child3->getPixelsPosition().y );
// All orders equal, so no order-based sort needed; direction-only reversal in drawChildren
SmallVector<Node*, 127> paintOrder;
flex->buildDrawOrderVector( paintOrder );
ASSERT_EQ( paintOrder.size(), 3u );
EXPECT_EQ( paintOrder[0], child1 );
EXPECT_EQ( paintOrder[1], child2 );
EXPECT_EQ( paintOrder[2], child3 );
// CSS Flexbox 4.3 uses order-modified document order, independent of flex-direction.
EXPECT_FALSE( flex->getNeedsOrderSort() );
Engine::destroySingleton();
}
UTEST( FlexContainer, directionReversePaintRowReverse ) {
UTEST( FlexContainer, directionReverseLayoutKeepsRowPaintOrder ) {
Engine::instance()->createWindow( WindowSettings( 1024, 650, "Flex Test", WindowStyle::Default,
WindowBackend::Default, 32, {}, 1, false,
true ),
@@ -2336,13 +2343,20 @@ UTEST( FlexContainer, directionReversePaintRowReverse ) {
EXPECT_GT( child1->getPixelsPosition().x, child2->getPixelsPosition().x );
EXPECT_GT( child2->getPixelsPosition().x, child3->getPixelsPosition().x );
// All orders equal so no order-based sort needed
SmallVector<Node*, 127> paintOrder;
flex->buildDrawOrderVector( paintOrder );
ASSERT_EQ( paintOrder.size(), 3u );
EXPECT_EQ( paintOrder[0], child1 );
EXPECT_EQ( paintOrder[1], child2 );
EXPECT_EQ( paintOrder[2], child3 );
// All orders equal, so the source order is also the order-modified document order.
EXPECT_FALSE( flex->getNeedsOrderSort() );
Engine::destroySingleton();
}
UTEST( FlexContainer, directionReversePaintWithOrderSort ) {
UTEST( FlexContainer, directionReverseLayoutUsesOrderModifiedPaintOrder ) {
Engine::instance()->createWindow( WindowSettings( 1024, 650, "Flex Test", WindowStyle::Default,
WindowBackend::Default, 32, {}, 1, false,
true ),
@@ -2388,6 +2402,38 @@ UTEST( FlexContainer, directionReversePaintWithOrderSort ) {
EXPECT_LT( child3->getPixelsPosition().y, child2->getPixelsPosition().y );
EXPECT_TRUE( flex->getNeedsOrderSort() );
SmallVector<Node*, 127> paintOrder;
flex->buildDrawOrderVector( paintOrder );
ASSERT_EQ( paintOrder.size(), 3u );
EXPECT_EQ( paintOrder[0], child2 );
EXPECT_EQ( paintOrder[1], child3 );
EXPECT_EQ( paintOrder[2], child1 );
Engine::destroySingleton();
}
UTEST( FlexContainer, floatDoesNotChangeFlexItemPaintPhase ) {
Engine::instance()->createWindow( WindowSettings( 640, 480, "Flex paint float",
WindowStyle::Default, WindowBackend::Default,
32, {}, 1, false, true ),
ContextSettings( false, 0, 0, GLv_default, true, false ) );
init_flex_test();
UISceneNode* sceneNode = SceneManager::instance()->getUISceneNode();
auto* flex = UIHTMLWidget::New();
flex->setParent( sceneNode->getRoot() );
flex->setDisplay( CSSDisplay::Flex );
auto* floated = UIHTMLWidget::New();
floated->setParent( flex );
floated->setCSSFloat( CSSFloat::Left );
auto* normal = UIHTMLWidget::New();
normal->setParent( flex );
sceneNode->updateDirtyLayouts();
SmallVector<Node*, 127> paintOrder;
flex->buildDrawOrderVector( paintOrder );
ASSERT_EQ( paintOrder.size(), 2u );
EXPECT_EQ( paintOrder[0], floated );
EXPECT_EQ( paintOrder[1], normal );
Engine::destroySingleton();
}
+265
View File
@@ -127,6 +127,271 @@ UTEST( UIHTMLFloat, property_GetPropertyString ) {
eeDelete( w );
}
UTEST( UIHTMLFloat, zIndexPreservesAutoAndApplicability ) {
auto* child = UIHTMLWidget::New();
EXPECT_TRUE( child->hasAutoZIndex() );
EXPECT_TRUE( child->getPropertyString( "z-index" ) == "auto" );
child->applyProperty( StyleSheetProperty( "z-index", "0" ) );
EXPECT_FALSE( child->hasAutoZIndex() );
EXPECT_EQ( child->getZIndex(), 0 );
EXPECT_FALSE( child->hasApplicableZIndex() );
child->setCSSPosition( CSSPosition::Relative );
EXPECT_TRUE( child->hasApplicableZIndex() );
EXPECT_TRUE( child->createsSupportedStackingGroup() );
child->applyProperty( StyleSheetProperty( "z-index", "-3" ) );
EXPECT_EQ( child->getZIndex(), -3 );
EXPECT_TRUE( child->getPropertyString( "z-index" ) == "-3" );
child->applyProperty( StyleSheetProperty( "z-index", "auto" ) );
EXPECT_TRUE( child->hasAutoZIndex() );
EXPECT_FALSE( child->createsSupportedStackingGroup() );
eeDelete( child );
}
UTEST( UIHTMLFloat, floatPaintsAndHitsAboveLaterNormalBlock ) {
init_float_test();
UISceneNode* sceneNode = SceneManager::instance()->getUISceneNode();
auto* parent = UIHTMLWidget::New();
parent->setParent( sceneNode->getRoot() );
parent->setPixelsSize( 300, 200 );
parent->setLayoutSizePolicy( SizePolicy::Fixed, SizePolicy::Fixed );
auto* side = UIHTMLWidget::New();
side->setParent( parent );
side->setPixelsSize( 100, 100 );
side->setLayoutSizePolicy( SizePolicy::Fixed, SizePolicy::Fixed );
side->setCSSFloat( CSSFloat::Right );
auto* sideLink = UIHTMLWidget::New();
sideLink->setParent( side );
sideLink->setPixelsSize( 100, 100 );
sideLink->setLayoutSizePolicy( SizePolicy::Fixed, SizePolicy::Fixed );
auto* content = UIHTMLWidget::New();
content->setParent( parent );
content->setPixelsSize( 300, 200 );
content->setLayoutSizePolicy( SizePolicy::Fixed, SizePolicy::Fixed );
SmallVector<Node*, 127> paintOrder;
parent->buildDrawOrderVector( paintOrder );
ASSERT_EQ( paintOrder.size(), 2u );
EXPECT_EQ( paintOrder[0], content );
EXPECT_EQ( paintOrder[1], side );
EXPECT_EQ( parent->getPaintOrderRebuildCount(), 1u );
paintOrder.clear();
parent->buildDrawOrderVector( paintOrder );
EXPECT_EQ( parent->getPaintOrderRebuildCount(), 1u );
EXPECT_EQ( parent->overFind( { 10, 10 } ), sideLink );
EXPECT_EQ( parent->getPaintOrderRebuildCount(), 1u );
side->setCSSFloat( CSSFloat::None );
paintOrder.clear();
parent->buildDrawOrderVector( paintOrder );
EXPECT_EQ( paintOrder[0], side );
EXPECT_EQ( paintOrder[1], content );
EXPECT_EQ( parent->getPaintOrderRebuildCount(), 2u );
Engine::destroySingleton();
}
UTEST( UIHTMLFloat, positionedDescendantParticipatesInRootStackingScope ) {
init_float_test();
UISceneNode* sceneNode = SceneManager::instance()->getUISceneNode();
auto makeBox = []( Node* parent ) {
auto* box = UIHTMLWidget::New();
box->setParent( parent );
box->setPixelsSize( 200, 150 );
box->setLayoutSizePolicy( SizePolicy::Fixed, SizePolicy::Fixed );
return box;
};
auto* root = makeBox( sceneNode->getRoot() );
auto* section = makeBox( root );
auto* popup = makeBox( section );
popup->setCSSPosition( CSSPosition::Absolute );
popup->setZIndex( 10 );
auto* overlay = makeBox( root );
overlay->setCSSPosition( CSSPosition::Relative );
overlay->setZIndex( 5 );
auto paintOrder = root->debugGetHTMLPaintOrder();
ASSERT_EQ( paintOrder.size(), 3u );
EXPECT_EQ( paintOrder[0], section );
EXPECT_EQ( paintOrder[1], overlay );
EXPECT_EQ( paintOrder[2], popup );
EXPECT_TRUE( popup->isHTMLPaintPromoted() );
EXPECT_EQ( root->overFind( { 10, 10 } ), popup );
section->setPixelsSize( 50, 50 );
section->setClipType( ClipType::ContentBox );
EXPECT_EQ( root->overFind( { 100, 100 } ), overlay );
section->setPixelsSize( 100, 100 );
section->setPadding( Rectf( 20, 20, 20, 20 ) );
section->setClipType( ClipType::PaddingBox );
EXPECT_EQ( root->overFind( { 10, 10 } ), overlay );
EXPECT_EQ( root->overFind( { 30, 30 } ), popup );
popup->setZIndexAuto();
paintOrder = root->debugGetHTMLPaintOrder();
ASSERT_EQ( paintOrder.size(), 3u );
EXPECT_EQ( paintOrder[0], section );
EXPECT_EQ( paintOrder[1], popup );
EXPECT_EQ( paintOrder[2], overlay );
EXPECT_TRUE( popup->isHTMLPaintPromoted() );
EXPECT_EQ( root->overFind( { 10, 10 } ), overlay );
Engine::destroySingleton();
}
UTEST( UIHTMLFloat, directStackingGroupIsAtomic ) {
init_float_test();
UISceneNode* sceneNode = SceneManager::instance()->getUISceneNode();
auto makeGroup = []( Node* parent, int zIndex ) {
auto* group = UIHTMLWidget::New();
group->setParent( parent );
group->setPixelsSize( 200, 150 );
group->setLayoutSizePolicy( SizePolicy::Fixed, SizePolicy::Fixed );
group->setCSSPosition( CSSPosition::Relative );
group->setZIndex( zIndex );
return group;
};
auto* root = UIHTMLWidget::New();
root->setParent( sceneNode->getRoot() );
root->setPixelsSize( 200, 150 );
auto* lowerGroup = makeGroup( root, 1 );
auto* highInsideLower = makeGroup( lowerGroup, 1000 );
auto* upperGroup = makeGroup( root, 2 );
const auto paintOrder = root->debugGetHTMLPaintOrder();
ASSERT_EQ( paintOrder.size(), 2u );
EXPECT_EQ( paintOrder[0], lowerGroup );
EXPECT_EQ( paintOrder[1], upperGroup );
EXPECT_EQ( root->overFind( { 10, 10 } ), upperGroup );
EXPECT_FALSE( highInsideLower->isHTMLPaintPromoted() );
Engine::destroySingleton();
}
UTEST( UIHTMLFloat, positionedDescendantPromotesThroughFloat ) {
init_float_test();
UISceneNode* sceneNode = SceneManager::instance()->getUISceneNode();
auto makeBox = []( Node* parent ) {
auto* box = UIHTMLWidget::New();
box->setParent( parent );
box->setPixelsSize( 200, 150 );
box->setLayoutSizePolicy( SizePolicy::Fixed, SizePolicy::Fixed );
return box;
};
auto* root = makeBox( sceneNode->getRoot() );
auto* floated = makeBox( root );
floated->setCSSFloat( CSSFloat::Left );
auto* popup = makeBox( floated );
popup->setCSSPosition( CSSPosition::Absolute );
popup->setZIndex( 10 );
auto* overlay = makeBox( root );
overlay->setCSSPosition( CSSPosition::Relative );
overlay->setZIndex( 5 );
const auto paintOrder = root->debugGetHTMLPaintOrder();
ASSERT_EQ( paintOrder.size(), 3u );
EXPECT_EQ( paintOrder[0], floated );
EXPECT_EQ( paintOrder[1], overlay );
EXPECT_EQ( paintOrder[2], popup );
EXPECT_EQ( root->overFind( { 10, 10 } ), popup );
Engine::destroySingleton();
}
UTEST( UIHTMLFloat, removingLastPromotionClearsSkipMetadata ) {
init_float_test();
UISceneNode* sceneNode = SceneManager::instance()->getUISceneNode();
auto* root = UIHTMLWidget::New();
root->setParent( sceneNode->getRoot() );
root->setPixelsSize( 200, 150 );
auto* section = UIHTMLWidget::New();
section->setParent( root );
section->setPixelsSize( 200, 150 );
auto* popup = UIHTMLWidget::New();
popup->setParent( section );
popup->setPixelsSize( 200, 150 );
popup->setCSSPosition( CSSPosition::Absolute );
popup->setZIndex( 10 );
root->debugGetHTMLPaintOrder();
EXPECT_TRUE( popup->isHTMLPaintPromoted() );
popup->setZIndexAuto();
popup->setCSSPosition( CSSPosition::Static );
root->debugGetHTMLPaintOrder();
EXPECT_FALSE( popup->isHTMLPaintPromoted() );
EXPECT_EQ( root->overFind( { 10, 10 } ), popup );
Engine::destroySingleton();
}
UTEST( UIHTMLFloat, nestedPositionedAutoParticipatesInAncestorPositionedPhase ) {
init_float_test();
UISceneNode* sceneNode = SceneManager::instance()->getUISceneNode();
auto* root = UIHTMLWidget::New();
root->setParent( sceneNode->getRoot() );
root->setPixelsSize( 200, 150 );
auto* section = UIHTMLWidget::New();
section->setParent( root );
section->setPixelsSize( 200, 150 );
auto* positionedAuto = UIHTMLWidget::New();
positionedAuto->setParent( section );
positionedAuto->setPixelsSize( 200, 150 );
positionedAuto->setCSSPosition( CSSPosition::Relative );
auto* floated = UIHTMLWidget::New();
floated->setParent( root );
floated->setPixelsSize( 200, 150 );
floated->setCSSFloat( CSSFloat::Left );
const auto paintOrder = root->debugGetHTMLPaintOrder();
ASSERT_EQ( paintOrder.size(), 3u );
EXPECT_EQ( paintOrder[0], section );
EXPECT_EQ( paintOrder[1], floated );
EXPECT_EQ( paintOrder[2], positionedAuto );
EXPECT_TRUE( positionedAuto->isHTMLPaintPromoted() );
EXPECT_EQ( root->overFind( { 10, 10 } ), positionedAuto );
Engine::destroySingleton();
}
UTEST( UIHTMLFloat, reparentingPromotedSubtreeClearsOldScopeMetadata ) {
init_float_test();
UISceneNode* sceneNode = SceneManager::instance()->getUISceneNode();
auto* oldRoot = UIHTMLWidget::New();
oldRoot->setParent( sceneNode->getRoot() );
oldRoot->setPixelsSize( 200, 150 );
auto* section = UIHTMLWidget::New();
section->setParent( oldRoot );
auto* popup = UIHTMLWidget::New();
popup->setParent( section );
popup->setPixelsSize( 100, 100 );
popup->setCSSPosition( CSSPosition::Absolute );
popup->setZIndex( 10 );
auto* newRoot = UIHTMLWidget::New();
newRoot->setParent( sceneNode->getRoot() );
newRoot->setPixelsSize( 200, 150 );
oldRoot->debugGetHTMLPaintOrder();
EXPECT_TRUE( popup->isHTMLPaintPromoted() );
popup->setParent( newRoot );
oldRoot->debugGetHTMLPaintOrder();
const auto newOrder = newRoot->debugGetHTMLPaintOrder();
ASSERT_EQ( newOrder.size(), 1u );
EXPECT_EQ( newOrder[0], popup );
EXPECT_FALSE( popup->isHTMLPaintPromoted() );
EXPECT_EQ( newRoot->overFind( { 10, 10 } ), popup );
Engine::destroySingleton();
}
UTEST( UIHTMLFloat, richtext_NoFloatLayout_NoChange ) {
init_float_test();
UISceneNode* sceneNode = SceneManager::instance()->getUISceneNode();