mirror of
https://github.com/SpartanJ/eepp.git
synced 2026-10-18 19:43:24 +03:00
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:
File diff suppressed because it is too large
Load Diff
@@ -241,7 +241,10 @@ Requirements:
|
||||
- **Gaps:** `row-gap`/`column-gap` with `gap` shorthand. `gap: normal` resolves to 0px in flexbox.
|
||||
- **`min-width: auto` / `min-height: auto`:** Content-based minimum prevents flex items from collapsing below their min-content size. The `overflow` property suppresses this minimum when set to non-visible.
|
||||
- **`visibility: collapse`:** Collapsed items are invisible and zero-sized on the main axis, but their cross size still contributes to the flex line cross size (preserving layout stability).
|
||||
- **`order`-modified paint order:** `UIHTMLWidget::drawChildren()` override stable-sorts children by CSS `order` when flex items have differing values. For `row-reverse`/`column-reverse`, the sorted list is also reversed before painting.
|
||||
- **`order`-modified paint order:** `UIHTMLWidget::drawChildren()` stable-sorts children by CSS
|
||||
`order` when flex items have differing values. Per CSS Flexbox §4.3, painting uses
|
||||
order-modified document order; `row-reverse`/`column-reverse` changes layout direction but does
|
||||
not reverse paint order.
|
||||
- **Container baseline:** After layout, the flex container's baseline is available via `FlexLayouter::getBaseline()` for use by outer formatting contexts.
|
||||
|
||||
### Out-Of-Flow Positioning
|
||||
@@ -257,6 +260,28 @@ Relative positioning should preserve normal-flow space and then offset painting/
|
||||
|
||||
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.
|
||||
|
||||
### Paint Order And Supported Stacking Contexts
|
||||
|
||||
`UIHTMLWidget` owns HTML box paint ordering and matching reverse-order hit testing. Its lazy paint
|
||||
cache implements the supported CSS 2 Appendix E phases: negative positioned stacking groups,
|
||||
in-flow content, non-positioned floats, positioned descendants with `z-index: auto` or `0`, and
|
||||
positive positioned stacking groups. Equal-category items retain CSS tree order; flex and grid
|
||||
containers use `order`-modified document order as their input tree order.
|
||||
|
||||
Positioned descendants and child stacking groups can participate in the nearest ancestor stacking
|
||||
scope even when they are nested below ordinary blocks or floats. Such promoted nodes remain in their
|
||||
DOM hierarchy for layout and ownership. Painting replays the skipped ancestors' transforms and exact
|
||||
border/content/padding clip type, and hit testing applies the same ancestor clips. A real stacking
|
||||
group is atomic: its descendant z-indices cannot escape and interleave with sibling groups.
|
||||
|
||||
The current stacking-context trigger subset consists of positioned boxes with an applicable
|
||||
non-`auto` `z-index`, fixed and sticky positioned boxes (including `z-index: auto`), and flex/grid
|
||||
items with non-`auto` `z-index`. Relative and absolute boxes with `z-index: auto` participate in the
|
||||
positioned-auto phase without becoming atomic. Opacity, transforms, filters, isolation, containment,
|
||||
and other modern stacking-context triggers are not yet modeled as CSS stacking groups. Inline
|
||||
fragment background/text sub-phases remain owned by `RichText`; implementing their complete Appendix
|
||||
E interleaving requires fragment-level paint records rather than widget-level sorting.
|
||||
|
||||
## Pixel Math
|
||||
|
||||
All layouters must use pixel (`Px`) APIs for layout calculations:
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 62 KiB |
@@ -12,6 +12,7 @@ class RichText;
|
||||
namespace EE { namespace UI {
|
||||
|
||||
class UILayouter;
|
||||
class UIHTMLWidget;
|
||||
|
||||
enum class CSSFormattingRole : Uint8 {
|
||||
Inline,
|
||||
@@ -30,6 +31,16 @@ struct CSSUsedMargins {
|
||||
Uint8 autoSides{ 0 };
|
||||
};
|
||||
|
||||
struct CSSZIndex {
|
||||
int value{ 0 };
|
||||
bool isAuto{ true };
|
||||
|
||||
bool operator==( const CSSZIndex& other ) const {
|
||||
return value == other.value && isAuto == other.isAuto;
|
||||
}
|
||||
bool operator!=( const CSSZIndex& other ) const { return !( *this == other ); }
|
||||
};
|
||||
|
||||
struct UIHTMLWidgetFlexState {
|
||||
CSSFlexDirection direction{ CSSFlexDirection::Row };
|
||||
CSSFlexWrap wrap{ CSSFlexWrap::NoWrap };
|
||||
@@ -62,6 +73,22 @@ struct UIHTMLWidgetGridState {
|
||||
CSSJustifySelf justifySelf{ CSSJustifySelf::Auto };
|
||||
};
|
||||
|
||||
enum class HTMLPaintCategory : Uint8 {
|
||||
NegativePositioned,
|
||||
NormalFlow,
|
||||
Float,
|
||||
PositionedAutoOrZero,
|
||||
PositivePositioned,
|
||||
};
|
||||
|
||||
struct UIHTMLPaintOrderCache {
|
||||
SmallVector<Node*, 16> items;
|
||||
Uint32 rebuildCount{ 0 };
|
||||
bool dirty{ true };
|
||||
};
|
||||
|
||||
using UIHTMLPaintAncestorVector = SmallVector<UIHTMLWidget*, 8>;
|
||||
|
||||
class EE_API UIHTMLWidget : public UILayout {
|
||||
public:
|
||||
static UIHTMLWidget* New();
|
||||
@@ -131,10 +158,22 @@ class EE_API UIHTMLWidget : public UILayout {
|
||||
|
||||
void setOffsets( const Rectf& offsets );
|
||||
|
||||
int getZIndex() const { return mZIndex; }
|
||||
const CSSZIndex& getCSSZIndex() const { return mZIndex; }
|
||||
|
||||
bool hasAutoZIndex() const { return mZIndex.isAuto; }
|
||||
|
||||
int getZIndex() const { return mZIndex.value; }
|
||||
|
||||
void setZIndex( int zIndex );
|
||||
|
||||
void setZIndexAuto();
|
||||
|
||||
bool isCSSPositioned() const { return mPosition != CSSPosition::Static; }
|
||||
|
||||
bool hasApplicableZIndex() const;
|
||||
|
||||
bool createsSupportedStackingGroup() const;
|
||||
|
||||
bool getNeedsOrderSort() const { return mNeedsOrderSort; }
|
||||
|
||||
void setNeedsOrderSort( bool val );
|
||||
@@ -308,10 +347,16 @@ class EE_API UIHTMLWidget : public UILayout {
|
||||
|
||||
virtual void onChildCountChange( Node* child, const bool& removed );
|
||||
|
||||
void updateZIndexSortFlag();
|
||||
|
||||
void buildDrawOrderVector( SmallVector<Node*, 127>& out ) const;
|
||||
|
||||
Uint32 getPaintOrderRebuildCount() const;
|
||||
|
||||
std::vector<Node*> debugGetHTMLPaintOrder() const;
|
||||
|
||||
std::vector<Node*> debugGetHTMLHitTestOrder() const;
|
||||
|
||||
bool isHTMLPaintPromoted() const { return mHTMLPaintOwner != nullptr; }
|
||||
|
||||
Float getBaseline() const;
|
||||
|
||||
Float getContainingBlockContentWidth() const;
|
||||
@@ -389,14 +434,20 @@ class EE_API UIHTMLWidget : public UILayout {
|
||||
std::string mBottomEq{ "auto" };
|
||||
std::string mLeftEq{ "auto" };
|
||||
Rectf mOffsets{ 0, 0, 0, 0 };
|
||||
int mZIndex{ 0 };
|
||||
CSSZIndex mZIndex;
|
||||
bool mOverflowCreatesBlockFormattingContext{ false };
|
||||
bool mNeedsOrderSort{ false };
|
||||
bool mNeedsZIndexSort{ false };
|
||||
bool mNeedsPaintOrder{ false };
|
||||
bool mHasStackingDescendant{ false };
|
||||
mutable bool mHasPromotedChild{ false };
|
||||
mutable bool mHasActivePromotions{ false };
|
||||
Uint8 mPercentageMargins{ 0 };
|
||||
UILayouter* mLayouter{ nullptr };
|
||||
UIHTMLWidgetFlexState* mFlexState{ nullptr };
|
||||
UIHTMLWidgetGridState* mGridState{ nullptr };
|
||||
mutable UIHTMLPaintOrderCache* mPaintOrderCache{ nullptr };
|
||||
mutable const UIHTMLWidget* mHTMLPaintOwner{ nullptr };
|
||||
mutable UIHTMLPaintAncestorVector* mHTMLPaintAncestors{ nullptr };
|
||||
UnorderedMap<std::string, StyleSheetProperty> mDataProperties;
|
||||
|
||||
Uint32 mScrollCb{ 0 };
|
||||
@@ -406,6 +457,22 @@ class EE_API UIHTMLWidget : public UILayout {
|
||||
|
||||
void updateScrollListeners();
|
||||
void onScrollTargetPositionChange();
|
||||
void invalidatePaintOrder();
|
||||
void updatePaintOrderFlag();
|
||||
const SmallVector<Node*, 16>& getPaintOrder() const;
|
||||
bool isHTMLStackingScope() const;
|
||||
void buildCSSChildOrder( SmallVector<Node*, 16>& out ) const;
|
||||
void resetPromotedPaintState() const;
|
||||
void collectStackingScopeItems( UIHTMLWidget* container, SmallVector<Node*, 16>& out,
|
||||
bool directChildren ) const;
|
||||
void collectStackingScopeChild( Node* child, SmallVector<Node*, 16>& out,
|
||||
bool directChildren ) const;
|
||||
void promoteHTMLPaintNode( UIHTMLWidget* widget ) const;
|
||||
void drawHTMLPaintNode( Node* node );
|
||||
bool containsHTMLPaintClipPoint( const Vector2f& point ) const;
|
||||
bool canHitHTMLPaintNode( Node* node, const Vector2f& point ) const;
|
||||
bool needsHTMLPaintTraversal() const;
|
||||
bool shouldSkipHTMLPaintNode( Node* node ) const;
|
||||
};
|
||||
|
||||
}} // namespace EE::UI
|
||||
|
||||
+337
-62
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user