mirror of
https://github.com/SpartanJ/eepp.git
synced 2026-10-01 18:50:16 +03:00
Merge remote-tracking branch 'origin/develop' into develop
This commit is contained in:
@@ -1483,9 +1483,4 @@ UITableCell* UIAbstractTableView::getCellFromIndex( const ModelIndex& index ) co
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void UIAbstractTableView::ColumnData::setWidth( Float w, bool manSet ) {
|
||||
width = w;
|
||||
manuallySet = manSet;
|
||||
}
|
||||
|
||||
}}} // namespace EE::UI::Abstract
|
||||
|
||||
+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