mirror of
https://github.com/SpartanJ/eepp.git
synced 2026-10-02 11:10:31 +03:00
Optimize UISceneNode style invalidation batching
Defer dirty-descendant coalescing until style processing instead of scanning the dirty set and walking widget ancestry on every invalidation. Preserve parent-first early-outs and resolve dirty roots against the current widget tree using shared snapshot storage. Clear stale style-state animation entries when widgets are deleted and add regression coverage for large invalidation bursts and reparenting.
This commit is contained in:
@@ -490,7 +490,8 @@ class EE_API UISceneNode : public SceneNode {
|
|||||||
* have its CSS re-applied during the next update cycle.
|
* have its CSS re-applied during the next update cycle.
|
||||||
*
|
*
|
||||||
* @param widget Pointer to the UIWidget to invalidate.
|
* @param widget Pointer to the UIWidget to invalidate.
|
||||||
* @param tryReinsert If true, attempts to reposition the widget in the dirty set.
|
* @param tryReinsert If true, re-evaluates the dirty-ancestor relationship for an already
|
||||||
|
* queued widget after reparenting. Descendant entries are coalesced during processing.
|
||||||
*/
|
*/
|
||||||
void invalidateStyle( UIWidget* widget, bool tryReinsert = false );
|
void invalidateStyle( UIWidget* widget, bool tryReinsert = false );
|
||||||
|
|
||||||
@@ -502,7 +503,9 @@ class EE_API UISceneNode : public SceneNode {
|
|||||||
*
|
*
|
||||||
* @param widget Pointer to the UIWidget to invalidate.
|
* @param widget Pointer to the UIWidget to invalidate.
|
||||||
* @param disableCSSAnimations If true, disables CSS animations during the update.
|
* @param disableCSSAnimations If true, disables CSS animations during the update.
|
||||||
* @param tryReinsert If true, attempts to reposition the widget in the dirty set.
|
* @param tryReinsert If true, re-evaluates the dirty-ancestor relationship and animation policy
|
||||||
|
* for an already queued widget after reparenting. Descendant entries are coalesced during
|
||||||
|
* processing.
|
||||||
*/
|
*/
|
||||||
void invalidateStyleState( UIWidget* widget, bool disableCSSAnimations = false,
|
void invalidateStyleState( UIWidget* widget, bool disableCSSAnimations = false,
|
||||||
bool tryReinsert = false );
|
bool tryReinsert = false );
|
||||||
@@ -985,7 +988,9 @@ class EE_API UISceneNode : public SceneNode {
|
|||||||
UnorderedSet<UIWidget*> mDirtyStyle;
|
UnorderedSet<UIWidget*> mDirtyStyle;
|
||||||
UnorderedSet<UIWidget*> mDirtyStyleState;
|
UnorderedSet<UIWidget*> mDirtyStyleState;
|
||||||
UnorderedMap<UIWidget*, bool> mDirtyStyleStateCSSAnimations;
|
UnorderedMap<UIWidget*, bool> mDirtyStyleStateCSSAnimations;
|
||||||
SmallVector<std::pair<UIWidget*, bool>, 64> mDirtyStyleStateSnapshot;
|
// Shared snapshot storage for style and style-state processing. The bool is ignored by the
|
||||||
|
// style pass and stores the disable-animations flag for the style-state pass.
|
||||||
|
SmallVector<std::pair<UIWidget*, bool>, 64> mDirtyStylesSnapshot;
|
||||||
UnorderedSet<UILayout*> mDirtyLayouts;
|
UnorderedSet<UILayout*> mDirtyLayouts;
|
||||||
SmallVector<UILayout*, 64> mDirtyLayoutsSnapshot;
|
SmallVector<UILayout*, 64> mDirtyLayoutsSnapshot;
|
||||||
std::vector<std::pair<Float, std::string>> mTimes;
|
std::vector<std::pair<Float, std::string>> mTimes;
|
||||||
|
|||||||
+48
-59
@@ -1241,6 +1241,7 @@ void UISceneNode::onWidgetDelete( Node* node ) {
|
|||||||
mDirtyStyle.erase( widget );
|
mDirtyStyle.erase( widget );
|
||||||
|
|
||||||
mDirtyStyleState.erase( widget );
|
mDirtyStyleState.erase( widget );
|
||||||
|
mDirtyStyleStateCSSAnimations.erase( widget );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1268,6 +1269,17 @@ UIWidget* UISceneNode::getRoot() const {
|
|||||||
return mRoot;
|
return mRoot;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
template <typename DirtyContainer>
|
||||||
|
static bool hasDirtyWidgetAncestor( const UIWidget* node, const DirtyContainer& dirty ) {
|
||||||
|
Node* parent = node->getParent();
|
||||||
|
while ( parent != nullptr ) {
|
||||||
|
if ( parent->isWidget() && dirty.count( parent->asType<UIWidget>() ) > 0 )
|
||||||
|
return true;
|
||||||
|
parent = parent->getParent();
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
void UISceneNode::invalidateStyle( UIWidget* node, bool tryReinsert ) {
|
void UISceneNode::invalidateStyle( UIWidget* node, bool tryReinsert ) {
|
||||||
eeASSERT( NULL != node );
|
eeASSERT( NULL != node );
|
||||||
|
|
||||||
@@ -1278,27 +1290,8 @@ void UISceneNode::invalidateStyle( UIWidget* node, bool tryReinsert ) {
|
|||||||
if ( alreadyExists && !tryReinsert )
|
if ( alreadyExists && !tryReinsert )
|
||||||
return;
|
return;
|
||||||
|
|
||||||
// Any parent dirty?
|
if ( hasDirtyWidgetAncestor( node, mDirtyStyle ) )
|
||||||
Node* parent = node->getParent();
|
return;
|
||||||
while ( parent != nullptr ) {
|
|
||||||
if ( parent->isWidget() && mDirtyStyle.count( parent->asType<UIWidget>() ) > 0 )
|
|
||||||
return;
|
|
||||||
parent = parent->getParent();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Now that we know we aren't early-outing, handle the reinsertion erase
|
|
||||||
if ( alreadyExists && tryReinsert )
|
|
||||||
mDirtyStyle.erase( node );
|
|
||||||
|
|
||||||
SmallVector<UIWidget*> eraseList;
|
|
||||||
|
|
||||||
// Any child in list? remove it
|
|
||||||
for ( auto widget : mDirtyStyle )
|
|
||||||
if ( NULL == widget || node->isParentOf( widget ) )
|
|
||||||
eraseList.push_back( widget );
|
|
||||||
|
|
||||||
for ( auto widget : eraseList )
|
|
||||||
mDirtyStyle.erase( widget );
|
|
||||||
|
|
||||||
mDirtyStyle.insert( node );
|
mDirtyStyle.insert( node );
|
||||||
}
|
}
|
||||||
@@ -1310,33 +1303,12 @@ void UISceneNode::invalidateStyleState( UIWidget* node, bool disableCSSAnimation
|
|||||||
if ( node->isClosing() )
|
if ( node->isClosing() )
|
||||||
return;
|
return;
|
||||||
|
|
||||||
// Already invalidated?
|
bool alreadyExists = mDirtyStyleState.count( node ) > 0;
|
||||||
if ( mDirtyStyleState.count( node ) > 0 ) {
|
if ( alreadyExists && !tryReinsert )
|
||||||
if ( !tryReinsert )
|
return;
|
||||||
return;
|
|
||||||
else
|
|
||||||
mDirtyStyleState.erase( node );
|
|
||||||
}
|
|
||||||
|
|
||||||
// Any parent dirty?
|
if ( hasDirtyWidgetAncestor( node, mDirtyStyleState ) )
|
||||||
Node* parent = node->getParent();
|
return;
|
||||||
while ( parent != nullptr ) {
|
|
||||||
if ( parent->isWidget() && mDirtyStyleState.count( parent->asType<UIWidget>() ) > 0 )
|
|
||||||
return;
|
|
||||||
parent = parent->getParent();
|
|
||||||
}
|
|
||||||
|
|
||||||
SmallVector<UIWidget*> eraseList;
|
|
||||||
|
|
||||||
// Any child in list? remove it
|
|
||||||
for ( auto widget : mDirtyStyleState )
|
|
||||||
if ( NULL == widget || node->isParentOf( widget ) )
|
|
||||||
eraseList.push_back( widget );
|
|
||||||
|
|
||||||
for ( auto widget : eraseList ) {
|
|
||||||
mDirtyStyleState.erase( widget );
|
|
||||||
mDirtyStyleStateCSSAnimations.erase( widget );
|
|
||||||
}
|
|
||||||
|
|
||||||
mDirtyStyleState.insert( node );
|
mDirtyStyleState.insert( node );
|
||||||
mDirtyStyleStateCSSAnimations[node] = disableCSSAnimations;
|
mDirtyStyleStateCSSAnimations[node] = disableCSSAnimations;
|
||||||
@@ -1461,11 +1433,26 @@ void UISceneNode::updateDirtyLayouts() {
|
|||||||
void UISceneNode::updateDirtyStyles() {
|
void UISceneNode::updateDirtyStyles() {
|
||||||
if ( !mDirtyStyle.empty() ) {
|
if ( !mDirtyStyle.empty() ) {
|
||||||
Clock clock;
|
Clock clock;
|
||||||
for ( auto& node : mDirtyStyle ) {
|
|
||||||
node->reloadStyle( true, false, false );
|
// Coalesce only once per pass. Eagerly searching the complete dirty set for descendants on
|
||||||
|
// every invalidation makes bursts quadratic and repeatedly pointer-chases unrelated widget
|
||||||
|
// ancestry. Retaining descendant entries until this point turns queueing into an ancestor
|
||||||
|
// walk plus an O(1) insertion. The current tree also naturally resolves reparented widgets.
|
||||||
|
//
|
||||||
|
// Clear the live set before applying styles: style application may create widgets or change
|
||||||
|
// selectors, and those invalidations must remain queued for the next invalidation-depth
|
||||||
|
// pass.
|
||||||
|
mDirtyStylesSnapshot.clear();
|
||||||
|
mDirtyStylesSnapshot.reserve( mDirtyStyle.size() );
|
||||||
|
for ( UIWidget* node : mDirtyStyle ) {
|
||||||
|
if ( node != nullptr && !hasDirtyWidgetAncestor( node, mDirtyStyle ) )
|
||||||
|
mDirtyStylesSnapshot.emplace_back( node, false );
|
||||||
}
|
}
|
||||||
mDirtyStyle.clear();
|
mDirtyStyle.clear();
|
||||||
|
|
||||||
|
for ( const auto& dirtyStyle : mDirtyStylesSnapshot )
|
||||||
|
dirtyStyle.first->reloadStyle( true, false, false );
|
||||||
|
|
||||||
if ( mVerbose )
|
if ( mVerbose )
|
||||||
Log::info( "CSS Styles Reloaded in %.2f ms", clock.getElapsedTime().asMilliseconds() );
|
Log::info( "CSS Styles Reloaded in %.2f ms", clock.getElapsedTime().asMilliseconds() );
|
||||||
}
|
}
|
||||||
@@ -1475,21 +1462,23 @@ void UISceneNode::updateDirtyStyleStates() {
|
|||||||
if ( !mDirtyStyleState.empty() ) {
|
if ( !mDirtyStyleState.empty() ) {
|
||||||
Clock clock;
|
Clock clock;
|
||||||
|
|
||||||
// Applying a style state can create widgets (for example a button icon). Widget
|
// Applying a style state can create widgets (for example a button icon). Coalesce the
|
||||||
// construction invalidates its style state, so iterating mDirtyStyleState directly would
|
// current roots into the shared snapshot, then leave new invalidations queued for the outer
|
||||||
// mutate and potentially reallocate its vector-backed unordered_dense storage. Snapshot the
|
// invalidation-depth loop. When both an ancestor and descendant are dirty, the ancestor's
|
||||||
// current pass and leave new invalidations queued for the outer invalidation-depth loop.
|
// animation policy wins, matching the previous eager-coalescing behavior.
|
||||||
mDirtyStyleStateSnapshot.clear();
|
mDirtyStylesSnapshot.clear();
|
||||||
mDirtyStyleStateSnapshot.reserve( mDirtyStyleState.size() );
|
mDirtyStylesSnapshot.reserve( mDirtyStyleState.size() );
|
||||||
for ( UIWidget* node : mDirtyStyleState ) {
|
for ( UIWidget* node : mDirtyStyleState ) {
|
||||||
auto animations = mDirtyStyleStateCSSAnimations.find( node );
|
if ( node != nullptr && !hasDirtyWidgetAncestor( node, mDirtyStyleState ) ) {
|
||||||
mDirtyStyleStateSnapshot.emplace_back(
|
auto animations = mDirtyStyleStateCSSAnimations.find( node );
|
||||||
node, animations != mDirtyStyleStateCSSAnimations.end() && animations->second );
|
mDirtyStylesSnapshot.emplace_back(
|
||||||
|
node, animations != mDirtyStyleStateCSSAnimations.end() && animations->second );
|
||||||
|
}
|
||||||
}
|
}
|
||||||
mDirtyStyleState.clear();
|
mDirtyStyleState.clear();
|
||||||
mDirtyStyleStateCSSAnimations.clear();
|
mDirtyStyleStateCSSAnimations.clear();
|
||||||
|
|
||||||
for ( const auto& dirtyState : mDirtyStyleStateSnapshot )
|
for ( const auto& dirtyState : mDirtyStylesSnapshot )
|
||||||
dirtyState.first->reportStyleStateChangeRecursive( dirtyState.second );
|
dirtyState.first->reportStyleStateChangeRecursive( dirtyState.second );
|
||||||
|
|
||||||
if ( mVerbose )
|
if ( mVerbose )
|
||||||
|
|||||||
@@ -29,22 +29,50 @@ UTEST( UISceneNode, CssPointerCursorUsesHandCursor ) {
|
|||||||
EXPECT_STREQ( Cursor::toName( Cursor::Arrow ), "arrow" );
|
EXPECT_STREQ( Cursor::toName( Cursor::Arrow ), "arrow" );
|
||||||
}
|
}
|
||||||
|
|
||||||
static UISceneNode* init_test_scene_node() {
|
static void init_test_scene_node( UISceneNode* sceneNode ) {
|
||||||
FileSystem::changeWorkingDirectory( Sys::getProcessPath() );
|
FileSystem::changeWorkingDirectory( Sys::getProcessPath() );
|
||||||
FontTrueType* font = FontTrueType::New( "NotoSans-Regular" ).get();
|
FontTrueType* font = FontTrueType::New( "NotoSans-Regular" ).get();
|
||||||
font->loadFromFile( "../assets/fonts/NotoSans-Regular.ttf" );
|
font->loadFromFile( "../assets/fonts/NotoSans-Regular.ttf" );
|
||||||
FontFamily::loadFromRegular( font );
|
FontFamily::loadFromRegular( font );
|
||||||
FontTrueType* monoFont = FontTrueType::New( "monospace" ).get();
|
FontTrueType* monoFont = FontTrueType::New( "monospace" ).get();
|
||||||
monoFont->loadFromFile( "../assets/fonts/NotoSans-Regular.ttf" );
|
monoFont->loadFromFile( "../assets/fonts/NotoSans-Regular.ttf" );
|
||||||
UISceneNode* sceneNode = UISceneNode::New();
|
|
||||||
SceneManager::instance()->add( sceneNode );
|
SceneManager::instance()->add( sceneNode );
|
||||||
SceneManager::instance()->setCurrentUISceneNode( sceneNode );
|
SceneManager::instance()->setCurrentUISceneNode( sceneNode );
|
||||||
UIThemeManager* themeManager = sceneNode->getUIThemeManager();
|
UIThemeManager* themeManager = sceneNode->getUIThemeManager();
|
||||||
themeManager->setDefaultFont( font );
|
themeManager->setDefaultFont( font );
|
||||||
themeManager->applyDefaultTheme( sceneNode->getRoot() );
|
themeManager->applyDefaultTheme( sceneNode->getRoot() );
|
||||||
|
}
|
||||||
|
|
||||||
|
static UISceneNode* init_test_scene_node() {
|
||||||
|
UISceneNode* sceneNode = UISceneNode::New();
|
||||||
|
init_test_scene_node( sceneNode );
|
||||||
return sceneNode;
|
return sceneNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class InvalidationTestSceneNode : public UISceneNode {
|
||||||
|
public:
|
||||||
|
static InvalidationTestSceneNode* New() { return eeNew( InvalidationTestSceneNode, () ); }
|
||||||
|
|
||||||
|
size_t pendingStyleCount() const { return mDirtyStyle.size(); }
|
||||||
|
|
||||||
|
size_t pendingStyleStateCount() const { return mDirtyStyleState.size(); }
|
||||||
|
|
||||||
|
size_t pendingStyleStateAnimationCount() const { return mDirtyStyleStateCSSAnimations.size(); }
|
||||||
|
|
||||||
|
size_t processedStyleRootCount() const { return mDirtyStylesSnapshot.size(); }
|
||||||
|
|
||||||
|
UIWidget* processedStyleRoot( size_t index ) const {
|
||||||
|
return mDirtyStylesSnapshot[index].first;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool processedStyleRootDisablesAnimations( size_t index ) const {
|
||||||
|
return mDirtyStylesSnapshot[index].second;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected:
|
||||||
|
InvalidationTestSceneNode() : UISceneNode() {}
|
||||||
|
};
|
||||||
|
|
||||||
UTEST( UISceneNode, ViewportMetricsAreIndependentFromSceneExtent ) {
|
UTEST( UISceneNode, ViewportMetricsAreIndependentFromSceneExtent ) {
|
||||||
Engine::instance()->createWindow( WindowSettings( 1024, 768, "Scene Viewport Metrics Test",
|
Engine::instance()->createWindow( WindowSettings( 1024, 768, "Scene Viewport Metrics Test",
|
||||||
WindowStyle::Default, WindowBackend::Default,
|
WindowStyle::Default, WindowBackend::Default,
|
||||||
@@ -394,6 +422,92 @@ UTEST( UISceneNode, StyleStateUpdateAllowsWidgetCreation ) {
|
|||||||
Engine::destroySingleton();
|
Engine::destroySingleton();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
UTEST( UISceneNode, StyleInvalidationCoalescesAtProcessingTime ) {
|
||||||
|
Engine::instance()->createWindow( WindowSettings( 1024, 768, "Deferred Style Invalidation Test",
|
||||||
|
WindowStyle::Default, WindowBackend::Default,
|
||||||
|
32, {}, 1, false, true ),
|
||||||
|
ContextSettings( false, 0, 0, GLv_default, true, false ) );
|
||||||
|
|
||||||
|
auto* sceneNode = InvalidationTestSceneNode::New();
|
||||||
|
init_test_scene_node( sceneNode );
|
||||||
|
sceneNode->flushDirtyStyleAndLayout();
|
||||||
|
|
||||||
|
UIWidget* parent = UIWidget::New();
|
||||||
|
parent->setParent( sceneNode->getRoot() );
|
||||||
|
sceneNode->flushDirtyStyleAndLayout();
|
||||||
|
|
||||||
|
constexpr size_t childCount = 2048;
|
||||||
|
UIWidget* firstChild = nullptr;
|
||||||
|
for ( size_t i = 0; i < childCount; ++i ) {
|
||||||
|
UIWidget* child = UIWidget::New();
|
||||||
|
child->setParent( parent );
|
||||||
|
if ( !firstChild )
|
||||||
|
firstChild = child;
|
||||||
|
}
|
||||||
|
|
||||||
|
EXPECT_EQ( childCount, sceneNode->pendingStyleCount() );
|
||||||
|
EXPECT_EQ( childCount, sceneNode->pendingStyleStateCount() );
|
||||||
|
|
||||||
|
sceneNode->invalidateStyle( parent );
|
||||||
|
sceneNode->invalidateStyleState( parent, true );
|
||||||
|
EXPECT_EQ( childCount + 1, sceneNode->pendingStyleCount() );
|
||||||
|
EXPECT_EQ( childCount + 1, sceneNode->pendingStyleStateCount() );
|
||||||
|
|
||||||
|
sceneNode->updateDirtyStyles();
|
||||||
|
EXPECT_EQ( size_t{ 1 }, sceneNode->processedStyleRootCount() );
|
||||||
|
EXPECT_EQ( parent, sceneNode->processedStyleRoot( 0 ) );
|
||||||
|
EXPECT_EQ( size_t{ 0 }, sceneNode->pendingStyleCount() );
|
||||||
|
|
||||||
|
sceneNode->updateDirtyStyleStates();
|
||||||
|
EXPECT_EQ( size_t{ 1 }, sceneNode->processedStyleRootCount() );
|
||||||
|
EXPECT_EQ( parent, sceneNode->processedStyleRoot( 0 ) );
|
||||||
|
EXPECT_TRUE( sceneNode->processedStyleRootDisablesAnimations( 0 ) );
|
||||||
|
EXPECT_EQ( size_t{ 0 }, sceneNode->pendingStyleStateCount() );
|
||||||
|
|
||||||
|
// The existing ancestor fast path still avoids queueing descendants when the parent arrives
|
||||||
|
// first, and the parent's animation policy governs the recursive state update.
|
||||||
|
sceneNode->invalidateStyle( parent );
|
||||||
|
sceneNode->invalidateStyle( firstChild );
|
||||||
|
sceneNode->invalidateStyleState( parent, false );
|
||||||
|
sceneNode->invalidateStyleState( firstChild, true );
|
||||||
|
EXPECT_EQ( size_t{ 1 }, sceneNode->pendingStyleCount() );
|
||||||
|
EXPECT_EQ( size_t{ 1 }, sceneNode->pendingStyleStateCount() );
|
||||||
|
sceneNode->updateDirtyStyles();
|
||||||
|
EXPECT_EQ( parent, sceneNode->processedStyleRoot( 0 ) );
|
||||||
|
sceneNode->updateDirtyStyleStates();
|
||||||
|
EXPECT_EQ( parent, sceneNode->processedStyleRoot( 0 ) );
|
||||||
|
EXPECT_FALSE( sceneNode->processedStyleRootDisablesAnimations( 0 ) );
|
||||||
|
|
||||||
|
// Reparenting an already-dirty widget under a dirty parent can leave both entries queued. The
|
||||||
|
// current tree must decide which root gets processed.
|
||||||
|
UIWidget* reparentTarget = UIWidget::New();
|
||||||
|
reparentTarget->setParent( sceneNode->getRoot() );
|
||||||
|
sceneNode->flushDirtyStyleAndLayout();
|
||||||
|
sceneNode->invalidateStyle( firstChild );
|
||||||
|
sceneNode->invalidateStyleState( firstChild );
|
||||||
|
sceneNode->invalidateStyle( reparentTarget );
|
||||||
|
sceneNode->invalidateStyleState( reparentTarget, true );
|
||||||
|
firstChild->setParent( reparentTarget );
|
||||||
|
sceneNode->updateDirtyStyles();
|
||||||
|
EXPECT_EQ( size_t{ 1 }, sceneNode->processedStyleRootCount() );
|
||||||
|
EXPECT_EQ( reparentTarget, sceneNode->processedStyleRoot( 0 ) );
|
||||||
|
sceneNode->updateDirtyStyleStates();
|
||||||
|
EXPECT_EQ( size_t{ 1 }, sceneNode->processedStyleRootCount() );
|
||||||
|
EXPECT_EQ( reparentTarget, sceneNode->processedStyleRoot( 0 ) );
|
||||||
|
EXPECT_TRUE( sceneNode->processedStyleRootDisablesAnimations( 0 ) );
|
||||||
|
|
||||||
|
UIWidget* deletedWidget = UIWidget::New();
|
||||||
|
deletedWidget->setParent( sceneNode->getRoot() );
|
||||||
|
sceneNode->flushDirtyStyleAndLayout();
|
||||||
|
sceneNode->invalidateStyleState( deletedWidget, true );
|
||||||
|
EXPECT_EQ( size_t{ 1 }, sceneNode->pendingStyleStateAnimationCount() );
|
||||||
|
eeDelete( deletedWidget );
|
||||||
|
EXPECT_EQ( size_t{ 0 }, sceneNode->pendingStyleStateCount() );
|
||||||
|
EXPECT_EQ( size_t{ 0 }, sceneNode->pendingStyleStateAnimationCount() );
|
||||||
|
|
||||||
|
Engine::destroySingleton();
|
||||||
|
}
|
||||||
|
|
||||||
UTEST( UIWindow, ModalWindowStopsKeyBindingsFromReachingScene ) {
|
UTEST( UIWindow, ModalWindowStopsKeyBindingsFromReachingScene ) {
|
||||||
Engine::instance()->createWindow( WindowSettings( 1024, 768, "Modal Key Binding Test",
|
Engine::instance()->createWindow( WindowSettings( 1024, 768, "Modal Key Binding Test",
|
||||||
WindowStyle::Default, WindowBackend::Default,
|
WindowStyle::Default, WindowBackend::Default,
|
||||||
|
|||||||
Reference in New Issue
Block a user