mirror of
https://github.com/SpartanJ/eepp.git
synced 2026-10-02 11:10:31 +03:00
Optimize stylesheet matching with class-based indexing:
Index class-anchored selectors by one mandatory class hash so widgets only evaluate rules associated with their own classes instead of scanning every class-only rule in the global bucket. Preserve selector correctness by validating the complete rule after candidate lookup, and retain stylesheet source order across independent index buckets for equal-specificity cascade resolution. Update index clearing, copying, marker removal, and style insertion paths. Add focused candidate coverage and a legacy full-scan benchmark, showing a 63% lookup-time reduction on a class-heavy stylesheet.
This commit is contained in:
@@ -631,6 +631,28 @@ Ensure class mutation APIs update hashes correctly.
|
|||||||
|
|
||||||
# Phase 6: Add Class-Based Stylesheet Index
|
# Phase 6: Add Class-Based Stylesheet Index
|
||||||
|
|
||||||
|
**Status: Implemented**
|
||||||
|
|
||||||
|
## Implementation State
|
||||||
|
|
||||||
|
- `StyleSheet` maintains a class-hash index in addition to the existing global, tag, ID, and
|
||||||
|
tag+ID index.
|
||||||
|
- Each style is owned by exactly one candidate bucket. The priority is ID/tag+ID, one rightmost
|
||||||
|
class anchor, tag, then global, so candidate collection does not require a per-element visited
|
||||||
|
allocation.
|
||||||
|
- Selector rules expose their sorted class hashes and currently select the first hash as the class
|
||||||
|
anchor.
|
||||||
|
- Class index state participates in clear, copy assignment, marker-based removal, and normal style
|
||||||
|
insertion paths.
|
||||||
|
- Per-stylesheet source-order metadata is used as the secondary sort key after specificity. This
|
||||||
|
preserves CSS cascade order when candidates originate from different index buckets.
|
||||||
|
- A focused candidate test covers global, tag, class-only, compound-class, tag+class, ID+class,
|
||||||
|
and unrelated-class rules.
|
||||||
|
- `Benchmark.CSSClassIndexLookup` exercises 1,024 class rules, of which 64 share a class with the
|
||||||
|
target widget. Across ten release runs, the legacy full scan had a 124.1 ms median and indexed
|
||||||
|
lookup had a 45.7 ms median for 20,000 lookups: a 63.2% reduction, or approximately 2.7x faster.
|
||||||
|
- The full release suite passes 735 tests with one skipped test.
|
||||||
|
|
||||||
## Motivation
|
## Motivation
|
||||||
|
|
||||||
This is expected to be the largest real-world win.
|
This is expected to be the largest real-world win.
|
||||||
|
|||||||
@@ -82,6 +82,13 @@ class EE_API StyleSheet {
|
|||||||
Uint32 mMarker{ 0 };
|
Uint32 mMarker{ 0 };
|
||||||
std::vector<std::shared_ptr<StyleSheetStyle>> mNodes;
|
std::vector<std::shared_ptr<StyleSheetStyle>> mNodes;
|
||||||
UnorderedMap<size_t, StyleSheetStyleVector> mNodeIndex;
|
UnorderedMap<size_t, StyleSheetStyleVector> mNodeIndex;
|
||||||
|
// Class-anchored rules live in one bucket only; selector matching validates tag and other
|
||||||
|
// classes.
|
||||||
|
UnorderedMap<String::HashType, StyleSheetStyleVector> mClassNodeIndex;
|
||||||
|
// Candidate buckets are independent, so retain insertion order explicitly for equal
|
||||||
|
// specificity.
|
||||||
|
UnorderedMap<const StyleSheetStyle*, size_t> mStyleSourceOrder;
|
||||||
|
size_t mNextStyleSourceOrder{ 0 };
|
||||||
MediaQueryList::vector mMediaQueryList;
|
MediaQueryList::vector mMediaQueryList;
|
||||||
KeyframesDefinitionMap mKeyframesMap;
|
KeyframesDefinitionMap mKeyframesMap;
|
||||||
using ElementDefinitionCache = UnorderedMap<size_t, std::shared_ptr<ElementDefinition>>;
|
using ElementDefinitionCache = UnorderedMap<size_t, std::shared_ptr<ElementDefinition>>;
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ class EE_API StyleSheetSelector {
|
|||||||
|
|
||||||
bool isStructurallyVolatile() const;
|
bool isStructurallyVolatile() const;
|
||||||
|
|
||||||
const StyleSheetSelectorRule& getRule( const Uint32& index );
|
const StyleSheetSelectorRule& getRule( const Uint32& index ) const;
|
||||||
|
|
||||||
const std::string& getSelectorId() const;
|
const std::string& getSelectorId() const;
|
||||||
|
|
||||||
|
|||||||
@@ -136,6 +136,22 @@ class EE_API StyleSheetSelectorRule {
|
|||||||
|
|
||||||
bool hasClass( const std::string& cls ) const;
|
bool hasClass( const std::string& cls ) const;
|
||||||
|
|
||||||
|
/** @return True when this rule requires at least one CSS class. */
|
||||||
|
bool hasClasses() const { return !mClassHashes.empty(); }
|
||||||
|
|
||||||
|
/** @return Sorted unique hashes of every CSS class required by this rule. */
|
||||||
|
const std::vector<String::HashType>& getClassHashes() const { return mClassHashes; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return The mandatory class used to place this rule in the stylesheet candidate index.
|
||||||
|
* Every matching element must have this class, but the full rule is still validated by
|
||||||
|
* matches(). The first sorted hash is used for now; a future implementation may choose the
|
||||||
|
* rarest class to reduce the candidate bucket further.
|
||||||
|
*/
|
||||||
|
String::HashType getBestClassHash() const {
|
||||||
|
return mClassHashes.empty() ? 0 : mClassHashes.front();
|
||||||
|
}
|
||||||
|
|
||||||
bool hasPseudoClasses() const;
|
bool hasPseudoClasses() const;
|
||||||
|
|
||||||
bool hasPseudoClass( const std::string& cls ) const;
|
bool hasPseudoClass( const std::string& cls ) const;
|
||||||
|
|||||||
@@ -781,7 +781,10 @@ class EE_API UIWidget : public UINode {
|
|||||||
/** @return Number of sorted unique CSS class hashes cached by this widget. */
|
/** @return Number of sorted unique CSS class hashes cached by this widget. */
|
||||||
inline Uint32 getClassHashCount() const { return static_cast<Uint32>( mClassHashes.size() ); }
|
inline Uint32 getClassHashCount() const { return static_cast<Uint32>( mClassHashes.size() ); }
|
||||||
|
|
||||||
/** @return True if all the sorted unique CSS class hashes are applied to this widget. */
|
/** @return Sorted unique class hashes used for stylesheet candidate lookup and matching. */
|
||||||
|
inline const SmallVector<String::HashType, 1>& getClassHashes() const { return mClassHashes; }
|
||||||
|
|
||||||
|
/** @return True if all the sorted unique required hashes are applied to this widget. */
|
||||||
inline bool hasClassHashes( const std::vector<String::HashType>& requiredHashes ) const {
|
inline bool hasClassHashes( const std::vector<String::HashType>& requiredHashes ) const {
|
||||||
const auto* hashes = mClassHashes.data();
|
const auto* hashes = mClassHashes.data();
|
||||||
const auto* hashesEnd = hashes + mClassHashes.size();
|
const auto* hashesEnd = hashes + mClassHashes.size();
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
#include <eepp/system/clock.hpp>
|
#include <eepp/system/clock.hpp>
|
||||||
#include <eepp/system/filesystem.hpp>
|
#include <eepp/system/filesystem.hpp>
|
||||||
#include <eepp/system/sys.hpp>
|
#include <eepp/system/sys.hpp>
|
||||||
|
#include <eepp/ui/css/stylesheetparser.hpp>
|
||||||
#include <eepp/ui/css/stylesheetselector.hpp>
|
#include <eepp/ui/css/stylesheetselector.hpp>
|
||||||
#include <eepp/ui/uimarkdownview.hpp>
|
#include <eepp/ui/uimarkdownview.hpp>
|
||||||
#include <eepp/ui/uiscenenode.hpp>
|
#include <eepp/ui/uiscenenode.hpp>
|
||||||
@@ -91,6 +92,64 @@ UTEST( Benchmark, CSSSelectorMatching ) {
|
|||||||
Engine::destroySingleton();
|
Engine::destroySingleton();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
UTEST( Benchmark, CSSClassIndexLookup ) {
|
||||||
|
Engine::instance()->createWindow( WindowSettings( 800, 600, "CSS class index bench",
|
||||||
|
WindowStyle::Default, WindowBackend::Default,
|
||||||
|
32, {}, 1, false, true ) );
|
||||||
|
UISceneNode* sceneNode = UISceneNode::New();
|
||||||
|
SceneManager::instance()->add( sceneNode );
|
||||||
|
|
||||||
|
UIWidget* widget = UIWidget::NewWithTag( "div" );
|
||||||
|
widget->setClasses( { "button", "primary", "toolbar-item" } );
|
||||||
|
widget->setParent( sceneNode->getRoot() );
|
||||||
|
|
||||||
|
static constexpr int selectorCount = 1024;
|
||||||
|
std::string css;
|
||||||
|
for ( int i = 0; i < selectorCount; ++i ) {
|
||||||
|
const std::string selector =
|
||||||
|
i % 16 == 0 ? ".button" : ".unmatched-class-" + String::toString( i );
|
||||||
|
css += selector + " { width: " + String::toString( i + 1 ) + "px; }\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
StyleSheetParser parser;
|
||||||
|
Clock constructionClock;
|
||||||
|
ASSERT_TRUE( parser.loadFromString( css ) );
|
||||||
|
const Time constructionElapsed = constructionClock.getElapsedTime();
|
||||||
|
|
||||||
|
const int matchingIterations = getSelectorMatchingIterations();
|
||||||
|
Uint64 legacyMatchCount = 0;
|
||||||
|
Clock legacyMatchingClock;
|
||||||
|
for ( int iteration = 0; iteration < matchingIterations; ++iteration ) {
|
||||||
|
for ( const auto& style : parser.getStyleSheet().getStyles() ) {
|
||||||
|
if ( style->isMediaValid() && style->getSelector().select( widget, false ) )
|
||||||
|
++legacyMatchCount;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const Time legacyMatchingElapsed = legacyMatchingClock.getElapsedTime();
|
||||||
|
|
||||||
|
Uint64 matchCount = 0;
|
||||||
|
Clock matchingClock;
|
||||||
|
for ( int iteration = 0; iteration < matchingIterations; ++iteration ) {
|
||||||
|
auto definition = parser.getStyleSheet().getElementStyles( widget, false );
|
||||||
|
matchCount += definition ? definition->getStyles().size() : 0;
|
||||||
|
}
|
||||||
|
const Time matchingElapsed = matchingClock.getElapsedTime();
|
||||||
|
|
||||||
|
EXPECT_EQ( static_cast<Uint64>( matchingIterations ) * 64u, legacyMatchCount );
|
||||||
|
EXPECT_EQ( legacyMatchCount, matchCount );
|
||||||
|
UTEST_PRINT_INFO(
|
||||||
|
String::format( "Stylesheet construction: %lld us", constructionElapsed.asMicroseconds() )
|
||||||
|
.c_str() );
|
||||||
|
UTEST_PRINT_INFO(
|
||||||
|
String::format( "Legacy full scan: %lld us", legacyMatchingElapsed.asMicroseconds() )
|
||||||
|
.c_str() );
|
||||||
|
UTEST_PRINT_INFO(
|
||||||
|
String::format( "Class index lookup: %lld us", matchingElapsed.asMicroseconds() ).c_str() );
|
||||||
|
UTEST_PRINT_INFO( String::format( "Stylesheet lookups: %d", matchingIterations ).c_str() );
|
||||||
|
|
||||||
|
Engine::destroySingleton();
|
||||||
|
}
|
||||||
|
|
||||||
static int getMarkdownFlushIterations() {
|
static int getMarkdownFlushIterations() {
|
||||||
if ( const char* env = std::getenv( "EE_MARKDOWN_BENCH_FLUSH_ITERATIONS" ) ) {
|
if ( const char* env = std::getenv( "EE_MARKDOWN_BENCH_FLUSH_ITERATIONS" ) ) {
|
||||||
Int32 val = markdownFlushIterations;
|
Int32 val = markdownFlushIterations;
|
||||||
|
|||||||
@@ -15,6 +15,9 @@ void StyleSheet::clear() {
|
|||||||
mMarker = 0;
|
mMarker = 0;
|
||||||
mNodes.clear();
|
mNodes.clear();
|
||||||
mNodeIndex.clear();
|
mNodeIndex.clear();
|
||||||
|
mClassNodeIndex.clear();
|
||||||
|
mStyleSourceOrder.clear();
|
||||||
|
mNextStyleSourceOrder = 0;
|
||||||
mMediaQueryList.clear();
|
mMediaQueryList.clear();
|
||||||
mKeyframesMap.clear();
|
mKeyframesMap.clear();
|
||||||
mNodeCache.clear();
|
mNodeCache.clear();
|
||||||
@@ -57,11 +60,18 @@ void StyleSheet::setMarker( const Uint32& marker ) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void StyleSheet::removeAllWithMarker( const Uint32& marker ) {
|
void StyleSheet::removeAllWithMarker( const Uint32& marker ) {
|
||||||
|
std::erase_if( mStyleSourceOrder,
|
||||||
|
[marker]( const auto& pair ) { return pair.first->getMarker() == marker; } );
|
||||||
std::erase_if( mNodeIndex, [marker]( auto& pair ) {
|
std::erase_if( mNodeIndex, [marker]( auto& pair ) {
|
||||||
std::erase_if( pair.second,
|
std::erase_if( pair.second,
|
||||||
[marker]( const auto& node ) { return node->getMarker() == marker; } );
|
[marker]( const auto& node ) { return node->getMarker() == marker; } );
|
||||||
return pair.second.empty(); // If true, the map entry is erased
|
return pair.second.empty(); // If true, the map entry is erased
|
||||||
} );
|
} );
|
||||||
|
std::erase_if( mClassNodeIndex, [marker]( auto& pair ) {
|
||||||
|
std::erase_if( pair.second,
|
||||||
|
[marker]( const auto& node ) { return node->getMarker() == marker; } );
|
||||||
|
return pair.second.empty();
|
||||||
|
} );
|
||||||
|
|
||||||
std::erase_if( mNodes, [marker]( const auto& node ) { return node->getMarker() == marker; } );
|
std::erase_if( mNodes, [marker]( const auto& node ) { return node->getMarker() == marker; } );
|
||||||
|
|
||||||
@@ -76,12 +86,19 @@ void StyleSheet::removeAllWithMarker( const Uint32& marker ) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void StyleSheet::removeAllWithoutMarker( const Uint32& marker ) {
|
void StyleSheet::removeAllWithoutMarker( const Uint32& marker ) {
|
||||||
|
std::erase_if( mStyleSourceOrder,
|
||||||
|
[marker]( const auto& pair ) { return pair.first->getMarker() != marker; } );
|
||||||
std::erase_if( mNodeIndex, [marker]( auto& pair ) {
|
std::erase_if( mNodeIndex, [marker]( auto& pair ) {
|
||||||
std::erase_if( pair.second, [marker]( const auto& node ) {
|
std::erase_if( pair.second, [marker]( const auto& node ) {
|
||||||
return node->getMarker() != marker; // Notice the !=
|
return node->getMarker() != marker; // Notice the !=
|
||||||
} );
|
} );
|
||||||
return pair.second.empty();
|
return pair.second.empty();
|
||||||
} );
|
} );
|
||||||
|
std::erase_if( mClassNodeIndex, [marker]( auto& pair ) {
|
||||||
|
std::erase_if( pair.second,
|
||||||
|
[marker]( const auto& node ) { return node->getMarker() != marker; } );
|
||||||
|
return pair.second.empty();
|
||||||
|
} );
|
||||||
|
|
||||||
std::erase_if( mNodes, [marker]( const auto& node ) { return node->getMarker() != marker; } );
|
std::erase_if( mNodes, [marker]( const auto& node ) { return node->getMarker() != marker; } );
|
||||||
|
|
||||||
@@ -165,6 +182,9 @@ StyleSheet& StyleSheet::operator=( const StyleSheet& other ) {
|
|||||||
mMarker = other.mMarker;
|
mMarker = other.mMarker;
|
||||||
mNodes = other.mNodes;
|
mNodes = other.mNodes;
|
||||||
mNodeIndex = other.mNodeIndex;
|
mNodeIndex = other.mNodeIndex;
|
||||||
|
mClassNodeIndex = other.mClassNodeIndex;
|
||||||
|
mStyleSourceOrder = other.mStyleSourceOrder;
|
||||||
|
mNextStyleSourceOrder = other.mNextStyleSourceOrder;
|
||||||
mMediaQueryList = other.mMediaQueryList;
|
mMediaQueryList = other.mMediaQueryList;
|
||||||
mKeyframesMap = other.mKeyframesMap;
|
mKeyframesMap = other.mKeyframesMap;
|
||||||
mNodeCache = other.mNodeCache;
|
mNodeCache = other.mNodeCache;
|
||||||
@@ -172,9 +192,25 @@ StyleSheet& StyleSheet::operator=( const StyleSheet& other ) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool StyleSheet::addStyleToNodeIndex( StyleSheetStyle* style ) {
|
bool StyleSheet::addStyleToNodeIndex( StyleSheetStyle* style ) {
|
||||||
const std::string& id = style->getSelector().getSelectorId();
|
|
||||||
const std::string& tag = style->getSelector().getSelectorTagName();
|
|
||||||
if ( style->hasProperties() || style->hasVariables() ) {
|
if ( style->hasProperties() || style->hasVariables() ) {
|
||||||
|
const auto& selector = style->getSelector();
|
||||||
|
const std::string& id = selector.getSelectorId();
|
||||||
|
const std::string& tag = selector.getSelectorTagName();
|
||||||
|
if ( id.empty() ) {
|
||||||
|
const auto& rule = selector.getRule( 0 );
|
||||||
|
if ( rule.hasClasses() ) {
|
||||||
|
// Use one mandatory class as the candidate anchor. Indexing every required class
|
||||||
|
// would duplicate the rule for elements that have several of them. The normal
|
||||||
|
// selector match below still validates the tag, all classes, and other constraints.
|
||||||
|
auto& nodes = mClassNodeIndex[rule.getBestClassHash()];
|
||||||
|
if ( std::find( nodes.begin(), nodes.end(), style ) == nodes.end() ) {
|
||||||
|
nodes.push_back( style );
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
Log::debug( "Ignored style %s", selector.getName().c_str() );
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
size_t nodeHash = this->nodeHash( "*" == tag ? "" : tag, id );
|
size_t nodeHash = this->nodeHash( "*" == tag ? "" : tag, id );
|
||||||
StyleSheetStyleVector& nodes = mNodeIndex[nodeHash];
|
StyleSheetStyleVector& nodes = mNodeIndex[nodeHash];
|
||||||
auto it = std::find( nodes.begin(), nodes.end(), style );
|
auto it = std::find( nodes.begin(), nodes.end(), style );
|
||||||
@@ -190,6 +226,7 @@ bool StyleSheet::addStyleToNodeIndex( StyleSheetStyle* style ) {
|
|||||||
|
|
||||||
void StyleSheet::addStyle( std::shared_ptr<StyleSheetStyle> node ) {
|
void StyleSheet::addStyle( std::shared_ptr<StyleSheetStyle> node ) {
|
||||||
if ( addStyleToNodeIndex( node.get() ) ) {
|
if ( addStyleToNodeIndex( node.get() ) ) {
|
||||||
|
mStyleSourceOrder[node.get()] = mNextStyleSourceOrder++;
|
||||||
mNodes.push_back( node );
|
mNodes.push_back( node );
|
||||||
}
|
}
|
||||||
addMediaQueryList( node->getMediaQueryList() );
|
addMediaQueryList( node->getMediaQueryList() );
|
||||||
@@ -229,10 +266,6 @@ void StyleSheet::combineStyleSheet( const StyleSheet& styleSheet ) {
|
|||||||
addKeyframes( styleSheet.getKeyframes() );
|
addKeyframes( styleSheet.getKeyframes() );
|
||||||
}
|
}
|
||||||
|
|
||||||
inline static bool StyleSheetNodeSort( const StyleSheetStyle* lhs, const StyleSheetStyle* rhs ) {
|
|
||||||
return lhs->getSelector().getSpecificity() < rhs->getSelector().getSpecificity();
|
|
||||||
}
|
|
||||||
|
|
||||||
// This is based on the RmlUi implementation.
|
// This is based on the RmlUi implementation.
|
||||||
std::shared_ptr<ElementDefinition> StyleSheet::getElementStyles( UIWidget* element,
|
std::shared_ptr<ElementDefinition> StyleSheet::getElementStyles( UIWidget* element,
|
||||||
const bool& applyPseudo ) const {
|
const bool& applyPseudo ) const {
|
||||||
@@ -266,7 +299,24 @@ std::shared_ptr<ElementDefinition> StyleSheet::getElementStyles( UIWidget* eleme
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
std::stable_sort( applicableNodes.begin(), applicableNodes.end(), StyleSheetNodeSort );
|
for ( auto classHash : element->getClassHashes() ) {
|
||||||
|
auto itNodes = mClassNodeIndex.find( classHash );
|
||||||
|
if ( itNodes == mClassNodeIndex.end() )
|
||||||
|
continue;
|
||||||
|
for ( StyleSheetStyle* node : itNodes->second ) {
|
||||||
|
if ( node->isMediaValid() && node->getSelector().select( element, applyPseudo ) )
|
||||||
|
applicableNodes.push_back( node );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::sort( applicableNodes.begin(), applicableNodes.end(),
|
||||||
|
[this]( const auto* lhs, const auto* rhs ) {
|
||||||
|
const auto lhsSpecificity = lhs->getSelector().getSpecificity();
|
||||||
|
const auto rhsSpecificity = rhs->getSelector().getSpecificity();
|
||||||
|
if ( lhsSpecificity != rhsSpecificity )
|
||||||
|
return lhsSpecificity < rhsSpecificity;
|
||||||
|
return mStyleSourceOrder.at( lhs ) < mStyleSourceOrder.at( rhs );
|
||||||
|
} );
|
||||||
|
|
||||||
if ( applicableNodes.empty() )
|
if ( applicableNodes.empty() )
|
||||||
return nullptr;
|
return nullptr;
|
||||||
|
|||||||
@@ -340,7 +340,7 @@ bool StyleSheetSelector::isStructurallyVolatile() const {
|
|||||||
return mStructurallyVolatile;
|
return mStructurallyVolatile;
|
||||||
}
|
}
|
||||||
|
|
||||||
const StyleSheetSelectorRule& StyleSheetSelector::getRule( const Uint32& index ) {
|
const StyleSheetSelectorRule& StyleSheetSelector::getRule( const Uint32& index ) const {
|
||||||
return mSelectorRules[index];
|
return mSelectorRules[index];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2914,6 +2914,40 @@ UTEST( UIHTML, HashedSelectorMatchingAndClassMutation ) {
|
|||||||
Engine::destroySingleton();
|
Engine::destroySingleton();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
UTEST( UIHTML, ClassIndexedStyleSheetCandidates ) {
|
||||||
|
Engine::instance()->createWindow( WindowSettings( 1024, 768, "Class Index Test",
|
||||||
|
WindowStyle::Default, WindowBackend::Default,
|
||||||
|
32, {}, 1, false, true ),
|
||||||
|
ContextSettings( false, 0, 0, GLv_default, true, false ) );
|
||||||
|
|
||||||
|
UISceneNode* sceneNode = init_test_inline_block();
|
||||||
|
sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML(
|
||||||
|
R"html(<html><body><div id="target" class="foo bar"></div></body></html>)html" ) );
|
||||||
|
auto* target = sceneNode->getRoot()->find( "target" )->asType<UIWidget>();
|
||||||
|
ASSERT_TRUE( target != nullptr );
|
||||||
|
|
||||||
|
StyleSheetParser parser;
|
||||||
|
ASSERT_TRUE( parser.loadFromString( std::string_view( R"css(
|
||||||
|
* { color: white; }
|
||||||
|
div { background-color: black; }
|
||||||
|
.foo { width: 10px; }
|
||||||
|
.foo.bar { height: 20px; }
|
||||||
|
div.foo { min-width: 5px; }
|
||||||
|
#target.foo { max-width: 50px; }
|
||||||
|
.unrelated { opacity: 0.5; }
|
||||||
|
)css" ) ) );
|
||||||
|
|
||||||
|
auto definition = parser.getStyleSheet().getElementStyles( target, false );
|
||||||
|
ASSERT_TRUE( definition != nullptr );
|
||||||
|
EXPECT_EQ( 6u, definition->getStyles().size() );
|
||||||
|
EXPECT_TRUE( std::none_of( definition->getStyles().begin(), definition->getStyles().end(),
|
||||||
|
[]( const StyleSheetStyle* style ) {
|
||||||
|
return style->getSelector().getName() == ".unrelated";
|
||||||
|
} ) );
|
||||||
|
|
||||||
|
Engine::destroySingleton();
|
||||||
|
}
|
||||||
|
|
||||||
UTEST( UIHTML, BlockList ) {
|
UTEST( UIHTML, BlockList ) {
|
||||||
Engine::instance()->createWindow( WindowSettings( 1024, 768, "Block List Test",
|
Engine::instance()->createWindow( WindowSettings( 1024, 768, "Block List Test",
|
||||||
WindowStyle::Default, WindowBackend::Default,
|
WindowStyle::Default, WindowBackend::Default,
|
||||||
|
|||||||
Reference in New Issue
Block a user