Reduce allocations in actions and CSS style updates

- allocate Action instances from reusable size-class pools
  - use inline storage for Runnable callbacks, action sequences, spawned actions,
    callback registrations, and main-thread action queues
  - preserve action factory and ownership semantics while recycling completed actions
  - cache computed transitions in ElementDefinition instead of reparsing them on
    every state change
  - parse transition and timing-function values through allocation-light string views
    and inline parameter storage
  - add a non-owning FunctionString view parser with callback-based parameter iteration
  - preserve nested functions, quoted parameters, escaped quotes, and early termination
  - fix transition timing-function lists to cycle independently from delay lists
  - avoid copying variable caches during CSS variable substitution
  - parse drawable sizes and positions without allocating split string vectors
  - reuse animation state and reduce temporary property parsing allocations
  - avoid memory-manager bookkeeping allocations for static source filenames
  - consolidate whitespace trimming through the existing String utilities
  - add regression coverage for action reuse, transition parsing, computed transition
    lookup, timing-list cycling, and non-owning FunctionString parsing
This commit is contained in:
Martín Lucas Golini
2026-08-06 01:16:52 -03:00
parent 74b0454721
commit d90ffe6fea
38 changed files with 1229 additions and 387 deletions
+8 -8
View File
@@ -18,7 +18,7 @@ using namespace EE::Window;
namespace EE {
AllocatedPointer::AllocatedPointer( void* data, const std::string& file, int line, size_t memory,
AllocatedPointer::AllocatedPointer( void* data, const char* file, int line, size_t memory,
bool track, bool globalAllocation ) {
mData = data;
mFile = file;
@@ -75,7 +75,7 @@ void* MemoryManager::addPointerInPlace( void* place, const AllocatedPointer& aAl
AllocatedPointerMapIt it = state.pointers.find( place );
if ( it != state.pointers.end() ) {
removePointer( place, aAllocatedPointer.mFile.c_str(), aAllocatedPointer.mLine );
removePointer( place, aAllocatedPointer.mFile, aAllocatedPointer.mLine );
}
return addPointer( aAllocatedPointer );
@@ -110,7 +110,7 @@ void* MemoryManager::addPointer( const AllocatedPointer& aAllocatedPointer ) {
if ( aAllocatedPointer.mTrack )
eePRINTL( "Allocating pointer %p at '%s' %d", aAllocatedPointer.mData,
aAllocatedPointer.mFile.c_str(), aAllocatedPointer.mLine );
aAllocatedPointer.mFile, aAllocatedPointer.mLine );
return aAllocatedPointer.mData;
}
@@ -123,7 +123,7 @@ void* MemoryManager::reallocPointer( void* data, const AllocatedPointer& aAlloca
AllocatedPointerMapIt it = state.pointers.find( data );
if ( it != state.pointers.end() && it->second.mTrack )
eePRINTL( "Realloc pointer %p at '%s' %d", data, aAllocatedPointer.mFile.c_str(),
eePRINTL( "Realloc pointer %p at '%s' %d", data, aAllocatedPointer.mFile,
aAllocatedPointer.mLine );
if ( it == state.pointers.end() )
@@ -131,10 +131,10 @@ void* MemoryManager::reallocPointer( void* data, const AllocatedPointer& aAlloca
if ( aAllocatedPointer.mTrack )
eePRINTL( "Reallocating pointer %p at '%s' %d", aAllocatedPointer.mData,
aAllocatedPointer.mFile.c_str(), aAllocatedPointer.mLine );
aAllocatedPointer.mFile, aAllocatedPointer.mLine );
if ( it->first != aAllocatedPointer.mData ) {
removePointer( data, aAllocatedPointer.mFile.c_str(), aAllocatedPointer.mLine );
removePointer( data, aAllocatedPointer.mFile, aAllocatedPointer.mLine );
addPointer( aAllocatedPointer );
} else {
state.totalMemoryUsage -= it->second.mMemory;
@@ -310,7 +310,7 @@ void MemoryManager::showResults() {
char address[2 + sizeof( void* ) * 2 + 1];
snprintf( address, sizeof( address ), "%p", ap.mData );
leaks.add_row( { address, ap.mFile, std::to_string( ap.mLine ),
leaks.add_row( { address, std::string{ ap.mFile }, std::to_string( ap.mLine ),
FileSystem::sizeToString( static_cast<Int64>( ap.mMemory ) ) } );
}
@@ -320,7 +320,7 @@ void MemoryManager::showResults() {
auto allocationLocation = []( const AllocatedPointer& allocation ) {
return allocation.mData == nullptr
? std::string( "-" )
: allocation.mFile + ":" + std::to_string( allocation.mLine );
: std::string{ allocation.mFile } + ":" + std::to_string( allocation.mLine );
};
tabulate::Table summary;
+142 -11
View File
@@ -1,8 +1,138 @@
#include <array>
#include <cstdlib>
#include <eepp/scene/action.hpp>
#include <eepp/scene/node.hpp>
#include <mutex>
#include <new>
namespace EE { namespace Scene {
namespace {
class ActionPool {
public:
static ActionPool& instance();
void* allocate( std::size_t size ) {
const std::size_t classIndex = findClass( size );
if ( classIndex == NumClasses )
return allocateLarge( size );
std::lock_guard<std::mutex> lock( mMutex );
SizeClass& sizeClass = mClasses[classIndex];
if ( nullptr == sizeClass.freeList )
allocatePage( classIndex );
BlockHeader* block = sizeClass.freeList;
sizeClass.freeList = block->next;
block->classIndex = static_cast<Uint16>( classIndex );
return block + 1;
}
void deallocate( void* ptr ) noexcept {
if ( nullptr == ptr )
return;
BlockHeader* block = static_cast<BlockHeader*>( ptr ) - 1;
if ( block->classIndex == LargeAllocation ) {
std::free( block );
return;
}
std::lock_guard<std::mutex> lock( mMutex );
SizeClass& sizeClass = mClasses[block->classIndex];
block->next = sizeClass.freeList;
sizeClass.freeList = block;
}
private:
static constexpr std::array<std::size_t, 9> ClassSizes = { 64, 96, 128, 192, 256,
384, 512, 768, 1024 };
static constexpr std::size_t NumClasses = ClassSizes.size();
static constexpr Uint16 LargeAllocation = static_cast<Uint16>( -1 );
static constexpr std::size_t BlocksPerPage = 64;
struct alignas( std::max_align_t ) BlockHeader {
BlockHeader* next;
Uint16 classIndex;
};
struct Page {
Page* next;
};
struct SizeClass {
BlockHeader* freeList{ nullptr };
Page* pages{ nullptr };
};
std::array<SizeClass, NumClasses> mClasses;
std::mutex mMutex;
static constexpr std::size_t alignUp( std::size_t size ) {
return ( size + alignof( std::max_align_t ) - 1 ) & ~( alignof( std::max_align_t ) - 1 );
}
static std::size_t findClass( std::size_t size ) {
for ( std::size_t i = 0; i < NumClasses; ++i ) {
if ( size <= ClassSizes[i] )
return i;
}
return NumClasses;
}
void allocatePage( std::size_t classIndex ) {
SizeClass& sizeClass = mClasses[classIndex];
const std::size_t stride = alignUp( sizeof( BlockHeader ) + ClassSizes[classIndex] );
const std::size_t bytes = alignUp( sizeof( Page ) ) + stride * BlocksPerPage;
Page* page = static_cast<Page*>( std::malloc( bytes ) );
if ( nullptr == page )
throw std::bad_alloc();
page->next = sizeClass.pages;
sizeClass.pages = page;
char* storage = reinterpret_cast<char*>( page ) + alignUp( sizeof( Page ) );
for ( std::size_t i = 0; i < BlocksPerPage; ++i ) {
BlockHeader* block = reinterpret_cast<BlockHeader*>( storage + stride * i );
block->classIndex = static_cast<Uint16>( classIndex );
block->next = sizeClass.freeList;
sizeClass.freeList = block;
}
}
static void* allocateLarge( std::size_t size ) {
BlockHeader* block =
static_cast<BlockHeader*>( std::malloc( sizeof( BlockHeader ) + size ) );
if ( nullptr == block )
throw std::bad_alloc();
block->classIndex = LargeAllocation;
block->next = nullptr;
return block + 1;
}
};
ActionPool& ActionPool::instance() {
// Actions can be owned by globals initialized before the first action allocation. Keeping the
// pool alive until process exit avoids a static-destruction-order race with those owners.
alignas( ActionPool ) static unsigned char storage[sizeof( ActionPool )];
static ActionPool* pool = new ( storage ) ActionPool;
return *pool;
}
} // namespace
void* Action::operator new( std::size_t size ) {
return ActionPool::instance().allocate( size );
}
void Action::operator delete( void* ptr ) noexcept {
ActionPool::instance().deallocate( ptr );
}
void Action::operator delete( void* ptr, std::size_t ) noexcept {
ActionPool::instance().deallocate( ptr );
}
Action::~Action() {
sendEvent( ActionType::OnDelete );
}
@@ -53,7 +183,7 @@ Action* Action::reverse() const {
Uint32 Action::addEventListener( const ActionType& actionType, const ActionCallback& callback ) {
mNumCallBacks++;
mCallbacks[actionType][mNumCallBacks] = callback;
mCallbacks.emplace_back( ActionCallbackEntry{ actionType, mNumCallBacks, callback } );
return mNumCallBacks;
}
@@ -65,22 +195,23 @@ Action* Action::on( const Action::ActionType& actionType, const Action::ActionCa
void Action::removeEventListener( const Uint32& callbackId ) {
for ( auto it = mCallbacks.begin(); it != mCallbacks.end(); ++it ) {
UnorderedMap<Uint32, ActionCallback>& event = it->second;
if ( event.erase( callbackId ) )
if ( it->id == callbackId ) {
mCallbacks.erase( it );
break;
}
}
}
void Action::sendEvent( const ActionType& actionType ) {
if ( 0 != mCallbacks.count( actionType ) ) {
auto event = mCallbacks[actionType];
if ( !event.empty() ) {
for ( auto it = event.begin(); it != event.end(); ++it )
it->second( this, actionType );
}
// Callbacks are allowed to mutate listener registration. Preserve the previous snapshot
// semantics while keeping the common callback count in inline storage.
SmallVector<ActionCallback, 4> callbacks;
for ( const auto& callback : mCallbacks ) {
if ( callback.type == actionType )
callbacks.emplace_back( callback.callback );
}
for ( auto& callback : callbacks )
callback( this, actionType );
}
void Action::onStart() {}
+11 -2
View File
@@ -81,12 +81,21 @@ std::vector<Action*> ActionManager::getActionsByTagFromTarget( Node* target,
return actions;
}
void ActionManager::getActionsByTagFromTarget( Node* target, const Action::UniqueID& tag,
SmallVector<Action*, 4>& actions ) {
Lock l( mMutex );
for ( Action* action : mActions ) {
if ( action->getTarget() == target && action->getTag() == tag )
actions.emplace_back( action );
}
}
bool ActionManager::removeActionByTag( const Action::UniqueID& tag ) {
return removeAction( getActionByTag( tag ) );
}
bool ActionManager::removeActionsByTagFromTarget( Node* target, const Action::UniqueID& tag ) {
std::vector<Action*> removeList;
ActionList removeList;
{
Lock l( mMutex );
@@ -226,7 +235,7 @@ bool ActionManager::removeActions( const std::vector<Action*>& actions ) {
}
bool ActionManager::removeAllActionsFromTarget( Node* target ) {
std::vector<Action*> removeList;
ActionList removeList;
{
Lock l( mMutex );
+1 -1
View File
@@ -4,7 +4,7 @@
namespace EE { namespace Scene { namespace Actions {
Runnable* Runnable::New( RunnableFunc callback, const Time& time, bool loop ) {
return eeNew( Runnable, ( callback, time, loop ) );
return eeNew( Runnable, ( std::move( callback ), time, loop ) );
}
Runnable::Runnable( Runnable::RunnableFunc callback, const Time& time, bool loop ) :
+19 -12
View File
@@ -7,41 +7,45 @@ Sequence* Sequence::New( const std::vector<Action*> sequence ) {
}
Sequence* Sequence::New( Action* action, Action* action2 ) {
return New( {action, action2} );
return eeNew( Sequence, ( ActionContainer{ action, action2 } ) );
}
Sequence* Sequence::New( Action* action, Action* action2, Action* action3 ) {
return New( {action, action2, action3} );
return eeNew( Sequence, ( ActionContainer{ action, action2, action3 } ) );
}
Sequence* Sequence::New( Action* action, Action* action2, Action* action3, Action* action4 ) {
return New( {action, action2, action3, action4} );
return eeNew( Sequence, ( ActionContainer{ action, action2, action3, action4 } ) );
}
Sequence* Sequence::New( Action* action, Action* action2, Action* action3, Action* action4,
Action* action5 ) {
return New( {action, action2, action3, action4, action5} );
return eeNew( Sequence, ( ActionContainer{ action, action2, action3, action4, action5 } ) );
}
Sequence* Sequence::New( Action* action, Action* action2, Action* action3, Action* action4,
Action* action5, Action* action6 ) {
return New( {action, action2, action3, action4, action5, action6} );
return eeNew( Sequence,
( ActionContainer{ action, action2, action3, action4, action5, action6 } ) );
}
Sequence* Sequence::New( Action* action, Action* action2, Action* action3, Action* action4,
Action* action5, Action* action6, Action* action7 ) {
return New( {action, action2, action3, action4, action5, action6, action7} );
return eeNew( Sequence, ( ActionContainer{ action, action2, action3, action4, action5, action6,
action7 } ) );
}
Sequence* Sequence::New( Action* action, Action* action2, Action* action3, Action* action4,
Action* action5, Action* action6, Action* action7, Action* action8 ) {
return New( {action, action2, action3, action4, action5, action6, action7, action8} );
return eeNew( Sequence, ( ActionContainer{ action, action2, action3, action4, action5, action6,
action7, action8 } ) );
}
Sequence* Sequence::New( Action* action, Action* action2, Action* action3, Action* action4,
Action* action5, Action* action6, Action* action7, Action* action8,
Action* action9 ) {
return New( {action, action2, action3, action4, action5, action6, action7, action8, action9} );
return eeNew( Sequence, ( ActionContainer{ action, action2, action3, action4, action5, action6,
action7, action8, action9 } ) );
}
void Sequence::start() {
@@ -102,17 +106,17 @@ Time Sequence::getTotalTime() {
}
Action* Sequence::clone() const {
return Sequence::New( mSequence );
return eeNew( Sequence, ( mSequence ) );
}
Action* Sequence::reverse() const {
std::vector<Action*> reversed;
ActionContainer reversed;
for ( auto it = mSequence.rbegin(); it != mSequence.rend(); ++it ) {
reversed.push_back( *it );
}
return Sequence::New( reversed );
return eeNew( Sequence, ( std::move( reversed ) ) );
}
Sequence::~Sequence() {
@@ -122,6 +126,9 @@ Sequence::~Sequence() {
}
}
Sequence::Sequence( const std::vector<Action*> sequence ) : mSequence( sequence ), mCurPos( 0 ) {}
Sequence::Sequence( const std::vector<Action*> sequence ) :
mSequence( sequence.begin(), sequence.end() ), mCurPos( 0 ) {}
Sequence::Sequence( ActionContainer sequence ) : mSequence( std::move( sequence ) ), mCurPos( 0 ) {}
}}} // namespace EE::Scene::Actions
+19 -12
View File
@@ -7,41 +7,45 @@ Spawn* Spawn::New( const std::vector<Action*> spawn ) {
}
Spawn* Spawn::New( Action* action, Action* action2 ) {
return New( {action, action2} );
return eeNew( Spawn, ( ActionContainer{ action, action2 } ) );
}
Spawn* Spawn::New( Action* action, Action* action2, Action* action3 ) {
return New( {action, action2, action3} );
return eeNew( Spawn, ( ActionContainer{ action, action2, action3 } ) );
}
Spawn* Spawn::New( Action* action, Action* action2, Action* action3, Action* action4 ) {
return New( {action, action2, action3, action4} );
return eeNew( Spawn, ( ActionContainer{ action, action2, action3, action4 } ) );
}
Spawn* Spawn::New( Action* action, Action* action2, Action* action3, Action* action4,
Action* action5 ) {
return New( {action, action2, action3, action4, action5} );
return eeNew( Spawn, ( ActionContainer{ action, action2, action3, action4, action5 } ) );
}
Spawn* Spawn::New( Action* action, Action* action2, Action* action3, Action* action4,
Action* action5, Action* action6 ) {
return New( {action, action2, action3, action4, action5, action6} );
return eeNew( Spawn,
( ActionContainer{ action, action2, action3, action4, action5, action6 } ) );
}
Spawn* Spawn::New( Action* action, Action* action2, Action* action3, Action* action4,
Action* action5, Action* action6, Action* action7 ) {
return New( {action, action2, action3, action4, action5, action6, action7} );
return eeNew( Spawn, ( ActionContainer{ action, action2, action3, action4, action5, action6,
action7 } ) );
}
Spawn* Spawn::New( Action* action, Action* action2, Action* action3, Action* action4,
Action* action5, Action* action6, Action* action7, Action* action8 ) {
return New( {action, action2, action3, action4, action5, action6, action7, action8} );
return eeNew( Spawn, ( ActionContainer{ action, action2, action3, action4, action5, action6,
action7, action8 } ) );
}
Spawn* Spawn::New( Action* action, Action* action2, Action* action3, Action* action4,
Action* action5, Action* action6, Action* action7, Action* action8,
Action* action9 ) {
return New( {action, action2, action3, action4, action5, action6, action7, action8, action9} );
return eeNew( Spawn, ( ActionContainer{ action, action2, action3, action4, action5, action6,
action7, action8, action9 } ) );
}
void Spawn::start() {
@@ -102,17 +106,17 @@ Time Spawn::getTotalTime() {
}
Action* Spawn::clone() const {
return Spawn::New( mSpawn );
return eeNew( Spawn, ( mSpawn ) );
}
Action* Spawn::reverse() const {
std::vector<Action*> reversed;
ActionContainer reversed;
for ( auto it = mSpawn.rbegin(); it != mSpawn.rend(); ++it ) {
reversed.push_back( *it );
}
return Spawn::New( reversed );
return eeNew( Spawn, ( std::move( reversed ) ) );
}
Spawn::~Spawn() {
@@ -122,6 +126,9 @@ Spawn::~Spawn() {
}
}
Spawn::Spawn( const std::vector<Action*> spawn ) : mSpawn( spawn ), mAllDone( false ) {}
Spawn::Spawn( const std::vector<Action*> spawn ) :
mSpawn( spawn.begin(), spawn.end() ), mAllDone( false ) {}
Spawn::Spawn( ActionContainer spawn ) : mSpawn( std::move( spawn ) ), mAllDone( false ) {}
}}} // namespace EE::Scene::Actions
+4
View File
@@ -1526,6 +1526,10 @@ std::vector<Action*> Node::getActionsByTag( const Action::UniqueID& tag ) {
return getActionManager()->getActionsByTagFromTarget( this, tag );
}
void Node::getActionsByTag( const Action::UniqueID& tag, SmallVector<Action*, 4>& actions ) {
getActionManager()->getActionsByTagFromTarget( this, tag, actions );
}
void Node::clearActions() {
getActionManager()->removeAllActionsFromTarget( this );
}
+2 -8
View File
@@ -106,10 +106,7 @@ static bool tryParseModernRgbRgba( std::string_view paramsStr, Color& outColor )
}
static bool parseHslHue( Float& out, std::string_view tok ) {
while ( !tok.empty() && ( tok.front() == ' ' || tok.front() == '\t' ) )
tok.remove_prefix( 1 );
while ( !tok.empty() && ( tok.back() == ' ' || tok.back() == '\t' ) )
tok.remove_suffix( 1 );
tok = String::trim( tok, " \t" );
if ( tok.empty() )
return false;
if ( tok.size() == 4 && ( std::strncmp( tok.data(), "none", 4 ) == 0 ||
@@ -133,10 +130,7 @@ static bool parseHslHue( Float& out, std::string_view tok ) {
}
static bool parseHslPercentOrNumber( Float& out, std::string_view tok ) {
while ( !tok.empty() && ( tok.front() == ' ' || tok.front() == '\t' ) )
tok.remove_prefix( 1 );
while ( !tok.empty() && ( tok.back() == ' ' || tok.back() == '\t' ) )
tok.remove_suffix( 1 );
tok = String::trim( tok, " \t" );
if ( tok.empty() )
return false;
if ( tok.size() == 4 && ( std::strncmp( tok.data(), "none", 4 ) == 0 ||
+34 -106
View File
@@ -3,128 +3,56 @@
#include <eepp/system/functionstring.hpp>
#include <string>
#include <type_traits>
namespace EE { namespace System {
template <typename StringType> FunctionString FunctionString::parse( StringType function ) {
using CharType = typename StringType::value_type;
size_t funcSep = function.find( '(' );
if ( funcSep == StringType::npos )
return FunctionString( "", {}, {} );
auto funcName = String::trim( function.substr( 0, funcSep ) );
FunctionString FunctionString::parseOwned( std::string_view function ) {
View view = parseView( function );
if ( view.isEmpty() )
return {};
Parameters funcParameters;
TypeStringVector typeStringData;
auto parametersString = function.substr( funcSep + 1 );
size_t paramClose = parametersString.find_last_of( ')' );
if ( paramClose == StringType::npos )
return FunctionString( "", {}, {} );
parametersString = parametersString.substr( 0, paramClose );
bool stateParsingString = false;
std::basic_string<CharType> buffer;
CharType prevChar = 0;
CharType quoteChar = 0;
bool currentParamIsString = false;
int parenDepth = 0;
auto pushBufferToParams = [&]() {
if constexpr ( std::is_same_v<CharType, char> ) {
if ( !currentParamIsString )
String::trimInPlace( buffer );
funcParameters.push_back( buffer );
} else {
std::string utf8Buffer = String( buffer ).toUtf8();
if ( !currentParamIsString )
String::trimInPlace( utf8Buffer );
funcParameters.push_back( utf8Buffer );
view.forEachParameter( [&]( std::string_view parameter, bool wasString ) {
std::string owned;
owned.reserve( parameter.size() );
const char quote = wasString ? function[parameter.data() - function.data() - 1] : 0;
for ( std::size_t i = 0; i < parameter.size(); ++i ) {
if ( wasString && parameter[i] == '\\' && i + 1 < parameter.size() &&
parameter[i + 1] == quote )
continue;
owned += parameter[i];
}
typeStringData.push_back( currentParamIsString );
buffer.clear();
currentParamIsString = false;
};
funcParameters.emplace_back( std::move( owned ) );
typeStringData.emplace_back( wasString );
return true;
} );
return FunctionString( std::string{ view.getName() }, std::move( funcParameters ),
std::move( typeStringData ) );
}
for ( size_t i = 0; i < parametersString.length(); ++i ) {
CharType c = parametersString[i];
if ( !stateParsingString ) {
if ( c == '(' ) {
parenDepth++;
buffer += c;
} else if ( c == ')' ) {
if ( parenDepth == 0 )
break;
if ( parenDepth > 0 )
parenDepth--;
buffer += c;
} else if ( c == ',' ) {
if ( parenDepth == 0 ) {
// Add param if we either accumulated characters or actively parsed an empty
// string parameter.
if ( !buffer.empty() || currentParamIsString ) {
pushBufferToParams();
}
} else {
buffer += c;
}
} else if ( c == '"' || c == '\'' ) {
stateParsingString = true;
quoteChar = c;
if ( parenDepth == 0 && buffer.empty() ) {
currentParamIsString = true;
} else {
buffer += c;
}
} else if ( c == ' ' ) {
if ( buffer.empty() || currentParamIsString )
continue;
else
buffer += c;
} else {
buffer += c;
}
} else {
if ( c == '\\' && i + 1 < parametersString.length() &&
parametersString[i + 1] == quoteChar ) {
} else if ( prevChar != '\\' && c == quoteChar ) {
stateParsingString = false;
if ( !currentParamIsString ) {
buffer += c;
}
} else {
buffer += c;
}
prevChar = c;
}
}
// Add param if we either accumulated characters or actively parsed an empty string parameter.
if ( !buffer.empty() || currentParamIsString )
pushBufferToParams();
if constexpr ( std::is_same_v<StringType, std::string> ) {
return FunctionString( std::string{ funcName }, funcParameters, typeStringData );
} else {
return FunctionString( String( funcName ).toUtf8(), funcParameters, typeStringData );
}
FunctionString::View FunctionString::parseView( std::string_view function ) {
const std::size_t open = function.find( '(' );
if ( open == std::string_view::npos )
return {};
const std::size_t close = function.rfind( ')' );
if ( close == std::string_view::npos || close < open )
return {};
std::string_view name = String::trim( function.substr( 0, open ), " \t\n\r\f\v" );
if ( name.empty() )
return {};
return View{ name, function.substr( open + 1, close - open - 1 ) };
}
FunctionString FunctionString::parse( const std::string& function ) {
return parse<std::string_view>( std::string_view{ function } );
return parseOwned( function );
}
FunctionString FunctionString::parse( std::string_view function ) {
return parse<std::string_view>( function );
return parseOwned( function );
}
FunctionString FunctionString::parse( String::View function ) {
return parse<String::View>( function );
return parseOwned( String( function ).toUtf8() );
}
FunctionString::FunctionString( const std::string& name, const Parameters& parameters,
+19 -1
View File
@@ -119,7 +119,8 @@ UnorderedMap<std::string, AnimationDefinition> AnimationDefinition::parseAnimati
case PropertyId::AnimationTimingFunction: {
TimingFunction tf( TimingFunction::parse( val ) );
timingFunctions.emplace_back( tf.interpolation );
timingFunctionParameters.emplace_back( tf.parameters );
timingFunctionParameters.emplace_back( tf.parameters.begin(),
tf.parameters.end() );
break;
}
default:
@@ -256,12 +257,29 @@ const String::HashType& AnimationDefinition::getId() const {
}
const std::vector<double>& AnimationDefinition::getTimingFunctionParameters() const {
if ( !mTimingFunctionParametersVectorValid ) {
mTimingFunctionParametersVector.assign( mTimingFunctionParameters.begin(),
mTimingFunctionParameters.end() );
mTimingFunctionParametersVectorValid = true;
}
return mTimingFunctionParametersVector;
}
const TimingFunction::Parameters& AnimationDefinition::getTimingFunctionParametersInline() const {
return mTimingFunctionParameters;
}
void AnimationDefinition::setTimingFunctionParameters(
const std::vector<double>& timingFunctionParameters ) {
mTimingFunctionParameters.assign( timingFunctionParameters.begin(),
timingFunctionParameters.end() );
mTimingFunctionParametersVectorValid = false;
}
void AnimationDefinition::setTimingFunctionParameters(
const TimingFunction::Parameters& timingFunctionParameters ) {
mTimingFunctionParameters = timingFunctionParameters;
mTimingFunctionParametersVectorValid = false;
}
const AnimationDefinition::AnimationFillMode& AnimationDefinition::getFillMode() const {
+5
View File
@@ -34,6 +34,10 @@ const std::vector<const StyleSheetProperty*>& ElementDefinition::getTransitionPr
return mTransitionProperties;
}
const ComputedTransitions& ElementDefinition::getTransitions() const {
return mTransitions;
}
const std::vector<const StyleSheetProperty*>& ElementDefinition::getAnimationProperties() const {
return mAnimationProperties;
}
@@ -81,6 +85,7 @@ void ElementDefinition::refresh() {
}
resolveVariables();
mTransitions = ComputedTransitions::parse( mTransitionProperties );
for ( auto& property : mProperties ) {
// Only registered properties participate in the dense ID set. Unknown
+11 -4
View File
@@ -232,6 +232,9 @@ StyleSheetLength::StyleSheetLength( const Float& val, const StyleSheetLength::Un
mUnit( unit ), mValue( val ) {}
StyleSheetLength::StyleSheetLength( const std::string& val, const Float& defaultValue ) :
StyleSheetLength( fromString( std::string_view{ val }, defaultValue ) ) {}
StyleSheetLength::StyleSheetLength( std::string_view val, const Float& defaultValue ) :
StyleSheetLength( fromString( val, defaultValue ) ) {}
StyleSheetLength::StyleSheetLength( const StyleSheetLength& val ) {
@@ -382,9 +385,14 @@ StyleSheetLength& StyleSheetLength::operator=( const StyleSheetLength& val ) {
StyleSheetLength StyleSheetLength::fromString( const std::string& str, const Float& defaultValue,
bool pxAsDp ) {
return fromString( std::string_view{ str }, defaultValue, pxAsDp );
}
StyleSheetLength StyleSheetLength::fromString( std::string_view str, const Float& defaultValue,
bool pxAsDp ) {
StyleSheetLength length;
length.setValue( defaultValue, Unit::Px );
const std::string_view value = String::trim( std::string_view( str ) );
const std::string_view value = String::trim( str );
if ( isFunctionString( value ) ) {
Unit funcUnit = Unit::Px;
@@ -470,9 +478,8 @@ bool StyleSheetLength::isFunctionString( std::string_view str ) {
String::istartsWith( str, "max(" ) || String::istartsWith( str, "calc(" );
}
bool StyleSheetLength::parseFunction( const std::string& str, Unit& outUnit, Arguments& outArgs ) {
std::string_view sv( str );
std::string_view trimmed = String::trim( sv );
bool StyleSheetLength::parseFunction( std::string_view str, Unit& outUnit, Arguments& outArgs ) {
std::string_view trimmed = String::trim( str );
if ( String::istartsWith( trimmed, "clamp(" ) ) {
outUnit = Unit::Clamp;
+56 -22
View File
@@ -11,7 +11,7 @@ using namespace EE::Math::easing;
namespace EE { namespace UI { namespace CSS {
inline Float easingFn( const Ease::Interpolation& timingFunction,
const std::vector<double>& timingFunctionParameters, const double& t,
const TimingFunction::Parameters& timingFunctionParameters, const double& t,
const double& b, const double& c, const double& d ) {
if ( timingFunction != Ease::Interpolation::CubizBezier )
return easingCb[timingFunction]( t, b, c, d );
@@ -24,13 +24,12 @@ inline Float easingFn( const Ease::Interpolation& timingFunction,
return t;
}
void StyleSheetPropertyAnimation::tweenProperty( UIWidget* widget, const Float& normalizedProgress,
const PropertyDefinition* property,
const std::string& startValue,
const std::string& endValue,
const Ease::Interpolation& timingFunction,
const std::vector<double> timingFunctionParameters,
const Uint32& propertyIndex, const bool& isDone ) {
void StyleSheetPropertyAnimation::tweenProperty(
UIWidget* widget, const Float& normalizedProgress, const PropertyDefinition* property,
const std::string& startValue, const std::string& endValue,
const Ease::Interpolation& timingFunction,
const TimingFunction::Parameters& timingFunctionParameters, const Uint32& propertyIndex,
const bool& isDone ) {
switch ( property->getType() ) {
case PropertyType::NumberFloat:
case PropertyType::NumberInt: {
@@ -220,13 +219,27 @@ StyleSheetPropertyAnimation* StyleSheetPropertyAnimation::New(
const Uint32& propertyIndex, const Time& duration, const Time& delay,
const Ease::Interpolation& timingFunction, const std::vector<double>& timingFunctionParameters,
const AnimationOrigin& animationOrigin ) {
TimingFunction::Parameters parameters;
parameters.assign( timingFunctionParameters.begin(), timingFunctionParameters.end() );
return New( property, startValue, endValue, propertyIndex, duration, delay, timingFunction,
parameters, animationOrigin );
}
StyleSheetPropertyAnimation*
StyleSheetPropertyAnimation::New( const PropertyDefinition* property, const std::string& startValue,
const std::string& endValue, const Uint32& propertyIndex,
const Time& duration, const Time& delay,
const Ease::Interpolation& timingFunction,
const TimingFunction::Parameters& timingFunctionParameters,
const AnimationOrigin& animationOrigin ) {
AnimationDefinition animation;
animation.setDelay( delay );
animation.setDuration( duration );
animation.setTimingFunction( timingFunction );
animation.setTimingFunctionParameters( timingFunctionParameters );
return New( animation, property, { startValue, endValue }, { 0, 1 }, propertyIndex,
animationOrigin );
return eeNew( StyleSheetPropertyAnimation,
( animation, property, SmallVector<std::string, 4>{ startValue, endValue },
SmallVector<Float, 4>{ 0, 1 }, propertyIndex, animationOrigin ) );
}
StyleSheetPropertyAnimation::StyleSheetPropertyAnimation( const AnimationDefinition& animation,
@@ -237,8 +250,26 @@ StyleSheetPropertyAnimation::StyleSheetPropertyAnimation( const AnimationDefinit
const AnimationOrigin& animationOrigin ) :
mAnimation( animation ),
mPropertyDef( propertyDef ),
mStates( states ),
mAnimationStepsTime( animationStepsTime ),
mStates( std::make_move_iterator( states.begin() ), std::make_move_iterator( states.end() ) ),
mAnimationStepsTime( std::make_move_iterator( animationStepsTime.begin() ),
std::make_move_iterator( animationStepsTime.end() ) ),
mPendingIterations( animation.getIterations() ),
mPropertyIndex( propertyIndex ),
mAnimationOrigin( animationOrigin ),
mPaused( mAnimation.isPaused() ) {
mId = ID;
}
StyleSheetPropertyAnimation::StyleSheetPropertyAnimation( const AnimationDefinition& animation,
const PropertyDefinition* propertyDef,
SmallVector<std::string, 4> states,
SmallVector<Float, 4> animationStepsTime,
const Uint32& propertyIndex,
const AnimationOrigin& animationOrigin ) :
mAnimation( animation ),
mPropertyDef( propertyDef ),
mStates( std::move( states ) ),
mAnimationStepsTime( std::move( animationStepsTime ) ),
mPendingIterations( animation.getIterations() ),
mPropertyIndex( propertyIndex ),
mAnimationOrigin( animationOrigin ),
@@ -321,15 +352,17 @@ Time StyleSheetPropertyAnimation::getTotalTime() {
}
Action* StyleSheetPropertyAnimation::clone() const {
return New( mAnimation, mPropertyDef, mStates, mAnimationStepsTime, mPropertyIndex,
mAnimationOrigin );
return eeNew( StyleSheetPropertyAnimation,
( mAnimation, mPropertyDef, mStates, mAnimationStepsTime, mPropertyIndex,
mAnimationOrigin ) );
}
Action* StyleSheetPropertyAnimation::reverse() const {
std::vector<std::string> vcopy( mStates );
SmallVector<std::string, 4> vcopy( mStates );
std::reverse( vcopy.begin(), vcopy.end() );
return New( mAnimation, mPropertyDef, vcopy, mAnimationStepsTime, mPropertyIndex,
mAnimationOrigin );
return eeNew( StyleSheetPropertyAnimation,
( mAnimation, mPropertyDef, std::move( vcopy ), mAnimationStepsTime,
mPropertyIndex, mAnimationOrigin ) );
}
const Uint32& StyleSheetPropertyAnimation::getPropertyIndex() const {
@@ -371,7 +404,8 @@ void StyleSheetPropertyAnimation::onUpdate( const Time& ) {
Float relativeProgress = curTime / relTime;
tweenProperty( widget, relativeProgress, mPropertyDef, mStates[curPos - 1],
mStates[curPos], mAnimation.getTimingFunction(),
mAnimation.getTimingFunctionParameters(), mPropertyIndex, isDone() );
mAnimation.getTimingFunctionParametersInline(), mPropertyIndex,
isDone() );
}
}
}
@@ -480,16 +514,16 @@ void StyleSheetPropertyAnimation::prepareDirection() {
}
void StyleSheetPropertyAnimation::reverseAnimation() {
std::vector<std::string> reverseCopy( mStates );
SmallVector<std::string, 4> reverseCopy( mStates );
std::reverse( reverseCopy.begin(), reverseCopy.end() );
mStates = reverseCopy;
mStates = std::move( reverseCopy );
std::vector<Float> reverseTimes( mAnimationStepsTime );
SmallVector<Float, 4> reverseTimes( mAnimationStepsTime );
std::reverse( reverseTimes.begin(), reverseTimes.end() );
for ( size_t i = 0; i < reverseTimes.size(); i++ ) {
reverseTimes[i] = 1.f - reverseTimes[i];
}
mAnimationStepsTime = reverseTimes;
mAnimationStepsTime = std::move( reverseTimes );
}
}}} // namespace EE::UI::CSS
+36 -20
View File
@@ -1,29 +1,45 @@
#include <eepp/system/functionstring.hpp>
#include <eepp/core/string.hpp>
#include <eepp/ui/css/timingfunction.hpp>
using namespace EE::System;
namespace EE { namespace UI { namespace CSS {
TimingFunction TimingFunction::parse( std::string timingFunction ) {
String::trimInPlace( timingFunction );
String::toLowerInPlace( timingFunction );
if ( timingFunction.find_first_of( '(' ) == std::string::npos ) {
return { Ease::fromName( timingFunction ), {} };
} else {
TimingFunction tf;
FunctionString func( FunctionString::parse( timingFunction ) );
if ( !func.isEmpty() ) {
tf.interpolation = Ease::fromName( timingFunction, Ease::Interpolation::None );
const auto& params = func.getParameters();
for ( const auto& param : params ) {
double d;
if ( String::fromString( d, param ) )
tf.parameters.emplace_back( d );
}
}
return tf;
return parse( std::string_view{ timingFunction } );
}
TimingFunction TimingFunction::parse( std::string_view timingFunction ) {
timingFunction = String::trim( timingFunction, " \t\n\r\f\v" );
SmallVector<char, 64> normalized;
for ( char character : timingFunction )
normalized.emplace_back( character >= 'A' && character <= 'Z' ? character + 32
: character );
timingFunction = std::string_view{ normalized.data(), normalized.size() };
TimingFunction result;
const auto open = timingFunction.find( '(' );
if ( open == std::string_view::npos ) {
result.interpolation = Ease::fromName( timingFunction, Ease::Interpolation::None );
return result;
}
const auto close = timingFunction.rfind( ')' );
if ( close == std::string_view::npos || close <= open )
return result;
result.interpolation =
Ease::fromName( timingFunction.substr( 0, open ), Ease::Interpolation::None );
std::string_view parameters = timingFunction.substr( open + 1, close - open - 1 );
while ( !parameters.empty() && result.parameters.size() < 4 ) {
const auto comma = parameters.find( ',' );
std::string_view parameter = String::trim( parameters.substr( 0, comma ), " \t\n\r\f\v" );
double value = 0;
if ( String::fromString( value, parameter ) )
result.parameters.emplace_back( value );
if ( comma == std::string_view::npos )
break;
parameters.remove_prefix( comma + 1 );
}
return result;
}
}}} // namespace EE::UI::CSS
+227 -101
View File
@@ -1,3 +1,4 @@
#include <cctype>
#include <eepp/core/string.hpp>
#include <eepp/system/functionstring.hpp>
#include <eepp/ui/css/propertydefinition.hpp>
@@ -6,122 +7,247 @@
namespace EE { namespace UI { namespace CSS {
namespace {
template <typename Callback> void forEachCommaItem( std::string_view value, Callback&& callback ) {
System::FunctionString::forEachParameter( value, [&]( std::string_view item, bool ) {
callback( item );
return true;
} );
}
SmallVector<std::string_view, 4> splitTransitionTokens( std::string_view value ) {
SmallVector<std::string_view, 4> tokens;
std::size_t start = 0;
int parenthesisDepth = 0;
for ( std::size_t i = 0; i <= value.size(); ++i ) {
if ( i < value.size() ) {
if ( value[i] == '(' )
++parenthesisDepth;
else if ( value[i] == ')' && parenthesisDepth > 0 )
--parenthesisDepth;
}
if ( i == value.size() ||
( std::isspace( static_cast<unsigned char>( value[i] ) ) && parenthesisDepth == 0 ) ) {
if ( i > start )
tokens.emplace_back( value.substr( start, i - start ) );
while ( i + 1 < value.size() &&
std::isspace( static_cast<unsigned char>( value[i + 1] ) ) )
++i;
start = i + 1;
}
}
return tokens;
}
Time parseTime( std::string_view value ) {
value = String::trim( value, " \t\n\r\f\v" );
auto lower = []( char character ) {
return character >= 'A' && character <= 'Z' ? character + ( 'a' - 'A' ) : character;
};
bool milliseconds = value.size() >= 2 && lower( value[value.size() - 2] ) == 'm' &&
lower( value.back() ) == 's';
bool seconds = !milliseconds && !value.empty() && lower( value.back() ) == 's';
bool minutes = !milliseconds && !seconds && !value.empty() && lower( value.back() ) == 'm';
std::size_t numberLength = value.size() - ( milliseconds ? 2 : ( seconds || minutes ? 1 : 0 ) );
double number = 0;
const char* numberStart = value.data();
if ( numberLength > 0 && *numberStart == '+' ) {
++numberStart;
--numberLength;
}
if ( !String::fromString( number, std::string_view{ numberStart, numberLength } ) )
return Time::Zero;
if ( milliseconds )
return Milliseconds( number );
if ( minutes )
return Minutes( number );
return Seconds( number );
}
String::HashType lowerHash( std::string_view value ) {
return String::hashToLower( value.data(), static_cast<Int64>( value.size() ) );
}
std::string lowerString( std::string_view value ) {
std::string result{ value };
String::toLowerInPlace( result );
return result;
}
} // namespace
UnorderedMap<std::string, TransitionDefinition> TransitionDefinition::parseTransitionProperties(
const std::vector<const StyleSheetProperty*>& styleSheetProperties ) {
std::vector<std::string> properties;
std::vector<Time> durations;
std::vector<Time> delays;
std::vector<Ease::Interpolation> timingFunctions;
std::vector<std::vector<double>> timingFunctionParameters;
SmallVector<std::string, 4> properties;
SmallVector<Time, 4> durations;
SmallVector<Time, 4> delays;
SmallVector<TimingFunction, 4> timingFunctions;
TransitionsMap transitions;
for ( auto& prop : styleSheetProperties ) {
if ( prop->getPropertyDefinition() == NULL )
for ( const StyleSheetProperty* property : styleSheetProperties ) {
if ( nullptr == property || nullptr == property->getPropertyDefinition() )
continue;
const PropertyDefinition* propDef = prop->getPropertyDefinition();
if ( propDef->getPropertyId() == PropertyId::Transition ) {
String::splitCb(
[&transitions, prop]( std::string_view str ) {
auto strTransition = String::trim( str );
auto splitTransition =
String::split( std::string{ strTransition }, " ", "", "()" );
if ( !splitTransition.empty() ) {
TransitionDefinition transitionDef;
if ( splitTransition.size() >= 2 ) {
std::string property = String::trim( splitTransition[0] );
String::toLowerInPlace( property );
Time duration =
StyleSheetProperty( prop->getName(),
String::toLower( splitTransition[1] ) )
.asTime();
transitionDef.property = property;
transitionDef.duration = duration;
if ( splitTransition.size() >= 3 ) {
TimingFunction tf( TimingFunction::parse( splitTransition[2] ) );
transitionDef.timingFunction = std::move( tf.interpolation );
transitionDef.timingFunctionParameters = std::move( tf.parameters );
if ( transitionDef.timingFunction == Ease::None &&
splitTransition.size() == 3 ) {
transitionDef.delay =
StyleSheetProperty( prop->getName(),
String::toLower( splitTransition[2] ) )
.asTime();
} else if ( splitTransition.size() >= 4 ) {
transitionDef.delay =
StyleSheetProperty( prop->getName(),
String::toLower( splitTransition[3] ) )
.asTime();
}
}
transitions[transitionDef.getProperty()] = transitionDef;
}
switch ( property->getPropertyDefinition()->getPropertyId() ) {
case PropertyId::Transition:
forEachCommaItem( property->getValue(), [&]( std::string_view item ) {
auto tokens = splitTransitionTokens( item );
if ( tokens.size() < 2 )
return;
TransitionDefinition transition;
transition.property = lowerString( tokens[0] );
transition.duration = parseTime( tokens[1] );
if ( tokens.size() >= 3 ) {
TimingFunction timing = TimingFunction::parse( tokens[2] );
transition.timingFunction = timing.interpolation;
transition.timingFunctionParameters.assign( timing.parameters.begin(),
timing.parameters.end() );
if ( transition.timingFunction == Ease::None && tokens.size() == 3 )
transition.delay = parseTime( tokens[2] );
else if ( tokens.size() >= 4 )
transition.delay = parseTime( tokens[3] );
}
return true;
},
prop->getValue(), ",", ",", "()" );
} else if ( propDef->getPropertyId() == PropertyId::TransitionDuration ) {
auto strDurations = String::split( prop->getValue(), ',' );
for ( auto dit = strDurations.begin(); dit != strDurations.end(); ++dit ) {
std::string duration( String::trim( *dit ) );
String::toLowerInPlace( duration );
durations.push_back( StyleSheetProperty( prop->getName(), duration ).asTime() );
}
} else if ( propDef->getPropertyId() == PropertyId::TransitionDelay ) {
auto strDelays = String::split( prop->getValue(), ',' );
for ( auto dit = strDelays.begin(); dit != strDelays.end(); ++dit ) {
std::string delay( String::trim( *dit ) );
String::toLowerInPlace( delay );
delays.push_back( StyleSheetProperty( prop->getName(), delay ).asTime() );
}
} else if ( propDef->getPropertyId() == PropertyId::TransitionTimingFunction ) {
auto strTimingFuncs = String::split( prop->getValue(), ",", "", "()" );
for ( auto dit = strTimingFuncs.begin(); dit != strTimingFuncs.end(); ++dit ) {
TimingFunction tf( TimingFunction::parse( *dit ) );
timingFunctions.emplace_back( tf.interpolation );
timingFunctionParameters.emplace_back( tf.parameters );
}
} else if ( propDef->getPropertyId() == PropertyId::TransitionProperty ) {
auto strProperties = String::split( prop->getValue(), ',' );
for ( auto dit = strProperties.begin(); dit != strProperties.end(); ++dit ) {
std::string property( String::trim( *dit ) );
String::toLowerInPlace( property );
properties.push_back( property );
}
transitions[transition.property] = std::move( transition );
} );
break;
case PropertyId::TransitionDuration:
forEachCommaItem( property->getValue(), [&]( std::string_view item ) {
durations.emplace_back( parseTime( item ) );
} );
break;
case PropertyId::TransitionDelay:
forEachCommaItem( property->getValue(), [&]( std::string_view item ) {
delays.emplace_back( parseTime( item ) );
} );
break;
case PropertyId::TransitionTimingFunction:
forEachCommaItem( property->getValue(), [&]( std::string_view item ) {
timingFunctions.emplace_back( TimingFunction::parse( item ) );
} );
break;
case PropertyId::TransitionProperty:
forEachCommaItem( property->getValue(), [&]( std::string_view item ) {
properties.emplace_back( lowerString( item ) );
} );
break;
default:
break;
}
}
for ( size_t i = 0; i < properties.size(); i++ ) {
const std::string& property = properties.at( i );
TransitionDefinition transitionDef;
transitionDef.property = property;
for ( std::size_t i = 0; i < properties.size(); ++i ) {
TransitionDefinition transition;
transition.property = properties[i];
if ( !durations.empty() )
transitionDef.duration = durations[i % durations.size()];
transition.duration = durations[i % durations.size()];
if ( !delays.empty() )
transitionDef.delay = delays[i % delays.size()];
transition.delay = delays[i % delays.size()];
if ( !timingFunctions.empty() ) {
size_t idx = !delays.empty() ? i % delays.size() : 0;
transitionDef.timingFunction = timingFunctions[idx];
transitionDef.timingFunctionParameters = timingFunctionParameters[idx];
const TimingFunction& timing = timingFunctions[i % timingFunctions.size()];
transition.timingFunction = timing.interpolation;
transition.timingFunctionParameters.assign( timing.parameters.begin(),
timing.parameters.end() );
}
transitions[transition.property] = std::move( transition );
}
transitions[property] = transitionDef;
return transitions;
}
void ComputedTransitions::set( const ComputedTransitionDefinition& transition ) {
for ( auto& current : mTransitions ) {
if ( current.propertyNameHash == transition.propertyNameHash ) {
current = transition;
return;
}
}
mTransitions.emplace_back( transition );
}
const ComputedTransitionDefinition*
ComputedTransitions::get( String::HashType propertyNameHash ) const {
const ComputedTransitionDefinition* all = nullptr;
for ( const auto& transition : mTransitions ) {
if ( transition.propertyNameHash == propertyNameHash )
return &transition;
if ( transition.propertyNameHash == String::hash( "all" ) )
all = &transition;
}
return all;
}
ComputedTransitions
ComputedTransitions::parse( const std::vector<const StyleSheetProperty*>& styleSheetProperties ) {
SmallVector<String::HashType, 4> properties;
SmallVector<Time, 4> durations;
SmallVector<Time, 4> delays;
SmallVector<TimingFunction, 4> timingFunctions;
ComputedTransitions transitions;
for ( const StyleSheetProperty* property : styleSheetProperties ) {
if ( nullptr == property || nullptr == property->getPropertyDefinition() )
continue;
switch ( property->getPropertyDefinition()->getPropertyId() ) {
case PropertyId::Transition:
forEachCommaItem( property->getValue(), [&]( std::string_view item ) {
auto tokens = splitTransitionTokens( item );
if ( tokens.size() < 2 )
return;
ComputedTransitionDefinition transition;
transition.propertyNameHash = lowerHash( tokens[0] );
transition.duration = parseTime( tokens[1] );
if ( tokens.size() >= 3 ) {
TimingFunction timing = TimingFunction::parse( tokens[2] );
transition.timingFunction = timing.interpolation;
transition.timingFunctionParameters = std::move( timing.parameters );
if ( transition.timingFunction == Ease::None && tokens.size() == 3 )
transition.delay = parseTime( tokens[2] );
else if ( tokens.size() >= 4 )
transition.delay = parseTime( tokens[3] );
}
transitions.set( transition );
} );
break;
case PropertyId::TransitionDuration:
forEachCommaItem( property->getValue(), [&]( std::string_view item ) {
durations.emplace_back( parseTime( item ) );
} );
break;
case PropertyId::TransitionDelay:
forEachCommaItem( property->getValue(), [&]( std::string_view item ) {
delays.emplace_back( parseTime( item ) );
} );
break;
case PropertyId::TransitionTimingFunction:
forEachCommaItem( property->getValue(), [&]( std::string_view item ) {
timingFunctions.emplace_back( TimingFunction::parse( item ) );
} );
break;
case PropertyId::TransitionProperty:
forEachCommaItem( property->getValue(), [&]( std::string_view item ) {
properties.emplace_back( lowerHash( item ) );
} );
break;
default:
break;
}
}
for ( std::size_t i = 0; i < properties.size(); ++i ) {
ComputedTransitionDefinition transition;
transition.propertyNameHash = properties[i];
if ( !durations.empty() )
transition.duration = durations[i % durations.size()];
if ( !delays.empty() )
transition.delay = delays[i % delays.size()];
if ( !timingFunctions.empty() ) {
const TimingFunction& timing = timingFunctions[i % timingFunctions.size()];
transition.timingFunction = timing.interpolation;
transition.timingFunctionParameters = timing.parameters;
}
transitions.set( transition );
}
return transitions;
+1 -8
View File
@@ -60,14 +60,7 @@ static bool hasDefiniteCSSHeight( UIWidget* widget ) {
static CSSBaselineAlignValue parseBaselineAlign( UIHTMLWidget* widget,
const StyleSheetProperty& property ) {
std::string_view val = property.value();
auto isSpace = []( char c ) {
return c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == '\f';
};
while ( !val.empty() && isSpace( val.front() ) )
val.remove_prefix( 1 );
while ( !val.empty() && isSpace( val.back() ) )
val.remove_suffix( 1 );
std::string_view val = String::trim( std::string_view{ property.value() }, " \t\n\r\f\v" );
if ( val.empty() )
return {};
+45 -17
View File
@@ -1,3 +1,5 @@
#include <array>
#include <cctype>
#include <eepp/core/core.hpp>
#include <eepp/graphics/drawableresource.hpp>
#include <eepp/graphics/image.hpp>
@@ -19,6 +21,36 @@ using namespace EE::Math::easing;
namespace EE { namespace UI {
namespace {
struct WhitespaceParts {
std::array<std::string_view, 3> values;
std::size_t count{ 0 };
std::size_t size() const { return count; }
bool empty() const { return count == 0; }
std::string_view operator[]( std::size_t index ) const { return values[index]; }
};
WhitespaceParts splitWhitespace( std::string_view value ) {
WhitespaceParts parts;
std::size_t position = 0;
while ( position < value.size() && parts.count < parts.values.size() ) {
while ( position < value.size() &&
std::isspace( static_cast<unsigned char>( value[position] ) ) )
++position;
const std::size_t start = position;
while ( position < value.size() &&
!std::isspace( static_cast<unsigned char>( value[position] ) ) )
++position;
if ( position > start )
parts.values[parts.count++] = value.substr( start, position - start );
}
return parts;
}
} // namespace
void UINodeDrawable::repeatFromText( const std::string& text, RepeatX& repeatX, RepeatY& repeatY ) {
auto parts = String::split( text, ' ' );
@@ -796,11 +828,9 @@ Sizef UINodeDrawable::LayerDrawable::calcDrawableSize( const std::string& drawab
Scale1 = eemax( Scale1, Scale2 );
size = Sizef( drawableSize.getWidth() * Scale1, drawableSize.getHeight() * Scale1 );
} else {
std::vector<std::string> sizePart = String::split( drawableSizeEq, ' ' );
if ( sizePart.size() == 1 ) {
sizePart.push_back( "auto" );
}
auto sizePart = splitWhitespace( drawableSizeEq );
if ( sizePart.size() == 1 )
sizePart.values[sizePart.count++] = "auto";
if ( sizePart.size() == 2 ) {
if ( sizePart[0] == "auto" && sizePart[1] == "auto" ) {
@@ -841,24 +871,22 @@ Sizef UINodeDrawable::LayerDrawable::calcDrawableSize( const std::string& drawab
return size;
}
Vector2f UINodeDrawable::LayerDrawable::calcPosition( std::string positionXEq,
std::string positionYEq ) {
Vector2f UINodeDrawable::LayerDrawable::calcPosition( const std::string& positionXEq,
const std::string& positionYEq ) {
Vector2f position( Vector2f::Zero );
if ( positionXEq.empty() )
positionXEq = "center";
if ( positionYEq.empty() )
positionYEq = "center";
auto posX = String::split( positionXEq, ' ' );
auto posY = String::split( positionYEq, ' ' );
const std::string_view positionX =
positionXEq.empty() ? std::string_view{ "center" } : std::string_view{ positionXEq };
const std::string_view positionY =
positionYEq.empty() ? std::string_view{ "center" } : std::string_view{ positionYEq };
auto posX = splitWhitespace( positionX );
auto posY = splitWhitespace( positionY );
if ( posX.empty() || posY.empty() )
return position;
bool needsRoundingX = positionXEq.back() == '%';
bool needsRoundingY = positionYEq.back() == '%';
bool needsRoundingX = positionX.back() == '%';
bool needsRoundingY = positionY.back() == '%';
Sizef ownerSize = mContainer->getOwner()->getPixelsSize();
Float refWidth = ownerSize.getWidth();
+33 -28
View File
@@ -135,8 +135,8 @@ void UIStyle::setStyleSheetProperties( const CSS::StyleSheetProperties& properti
}
bool UIStyle::hasTransition( const std::string& propertyName ) {
return mTransitions.find( propertyName ) != mTransitions.end() ||
mTransitions.find( "all" ) != mTransitions.end();
return mDefinition && mDefinition->getTransitions().get( String::hashToLower(
propertyName.c_str(), static_cast<Int64>( propertyName.size() ) ) );
}
StyleSheetPropertyAnimation* UIStyle::getAnimation( const PropertyDefinition* propertyDef ) {
@@ -160,15 +160,20 @@ bool UIStyle::hasAnimation( const PropertyDefinition* propertyDef ) {
}
TransitionDefinition UIStyle::getTransition( const std::string& propertyName ) {
auto propertyTransitionIt = mTransitions.find( propertyName );
if ( propertyTransitionIt != mTransitions.end() ) {
return propertyTransitionIt->second;
} else if ( ( propertyTransitionIt = mTransitions.find( "all" ) ) != mTransitions.end() ) {
return propertyTransitionIt->second;
}
return TransitionDefinition();
TransitionDefinition transition;
if ( !mDefinition )
return transition;
const auto* computed = mDefinition->getTransitions().get(
String::hashToLower( propertyName.c_str(), static_cast<Int64>( propertyName.size() ) ) );
if ( !computed )
return transition;
transition.property = propertyName;
transition.timingFunction = computed->timingFunction;
transition.timingFunctionParameters.assign( computed->timingFunctionParameters.begin(),
computed->timingFunctionParameters.end() );
transition.delay = computed->delay;
transition.duration = computed->duration;
return transition;
}
const bool& UIStyle::isChangingState() const {
@@ -339,9 +344,9 @@ void UIStyle::setVariableFromValue( StyleSheetProperty* property, const std::str
for ( int depth = 0; depth < maxDepth; depth++ ) {
bool changed = false;
if ( !property->getVarCache().empty() ) {
auto varCacheCopy = property->getVarCache();
for ( auto& var : varCacheCopy ) {
for ( auto& val : var.variableList ) {
const auto& varCache = property->getVarCache();
for ( const auto& var : varCache ) {
for ( const auto& val : var.variableList ) {
StyleSheetVariable variable( getVariable( val ) );
if ( !variable.isEmpty() ) {
String::replaceAll( newValue, var.definition, variable.getValue() );
@@ -432,11 +437,6 @@ void UIStyle::onStateChange() {
mWidget->beginAttributesTransaction();
if ( nullptr != mDefinition && !mDefinition->getTransitionProperties().empty() ) {
mTransitions = TransitionDefinition::parseTransitionProperties(
mDefinition->getTransitionProperties() );
}
for ( PropertyId prop : changedProperties ) {
std::optional<StyleSheetProperty> resolvedProperty;
StyleSheetProperty* property = getResolvedLocalProperty( prop, resolvedProperty );
@@ -669,11 +669,17 @@ void UIStyle::applyStyleSheetProperty( const StyleSheetProperty& originalPropert
}
}
TransitionDefinition transitionInfo( getTransition( property->getName() ) );
const ComputedTransitionDefinition* transitionInfo =
mDefinition ? mDefinition->getTransitions().get( propertyDefinition->getId() )
: nullptr;
if ( nullptr == transitionInfo ) {
mWidget->applyProperty( *property );
return;
}
std::vector<Action*> previousTransitions =
mWidget->getActionsByTag( propertyDefinition->getId() );
std::vector<Action*> removeTransitions;
SmallVector<Action*, 4> previousTransitions;
mWidget->getActionsByTag( propertyDefinition->getId(), previousTransitions );
SmallVector<Action*, 4> removeTransitions;
StyleSheetPropertyAnimation* prevTransition = NULL;
if ( !previousTransitions.empty() ) {
@@ -707,9 +713,9 @@ void UIStyle::applyStyleSheetProperty( const StyleSheetProperty& originalPropert
currentProgress = eemin( 1.f, currentProgress );
if ( 0.f != currentProgress ) {
elapsed = Milliseconds( ( 1.f - currentProgress ) *
transitionInfo.getDuration().asMilliseconds() );
transitionInfo->duration.asMilliseconds() );
} else {
elapsed = transitionInfo.getDuration();
elapsed = transitionInfo->duration;
}
startValue = prevTransition->getEndValue();
} else if ( startValue == prevTransition->getEndValue() ) {
@@ -723,9 +729,8 @@ void UIStyle::applyStyleSheetProperty( const StyleSheetProperty& originalPropert
StyleSheetPropertyAnimation* newTransition = StyleSheetPropertyAnimation::New(
propertyDefinition, startValue, property->getValue(), property->getIndex(),
transitionInfo.getDuration(), transitionInfo.getDelay(),
transitionInfo.getTimingFunction(), transitionInfo.getTimingFunctionParameters(),
AnimationOrigin::Transition );
transitionInfo->duration, transitionInfo->delay, transitionInfo->timingFunction,
transitionInfo->timingFunctionParameters, AnimationOrigin::Transition );
newTransition->setElapsed( elapsed );
newTransition->setTag( propertyDefinition->getId() );
mWidget->runAction( newTransition );
@@ -0,0 +1,125 @@
#include "utest.h"
#include <eepp/scene/actions/delay.hpp>
#include <eepp/scene/actions/runnable.hpp>
#include <eepp/ui/css/stylesheetspecification.hpp>
#include <eepp/ui/css/transitiondefinition.hpp>
using namespace EE;
using namespace EE::Math;
using namespace EE::Scene;
using namespace EE::Scene::Actions;
using namespace EE::System;
using namespace EE::UI::CSS;
UTEST( ActionPool, reusesCompletedActionSlots ) {
Delay* first = Delay::New( Milliseconds( 1 ) );
void* firstAddress = first;
eeDelete( first );
Delay* second = Delay::New( Milliseconds( 2 ) );
EXPECT_EQ( static_cast<void*>( second ), firstAddress );
eeDelete( second );
}
UTEST( ActionCallbacks, listenerMutationUsesEventSnapshot ) {
Delay* action = Delay::New( Time::Zero );
int callbackCount = 0;
Uint32 firstCallback = 0;
Uint32 secondCallback = 0;
firstCallback = action->addEventListener( Action::OnStart, [&]( Action* sender, const auto& ) {
++callbackCount;
sender->removeEventListener( secondCallback );
} );
secondCallback = action->addEventListener( Action::OnStart, [&]( Action* sender, const auto& ) {
++callbackCount;
sender->removeEventListener( firstCallback );
} );
action->start();
EXPECT_EQ( callbackCount, 2 );
eeDelete( action );
}
UTEST( Transitions, compactShorthandParsesWithoutLosingSemantics ) {
auto* specification = StyleSheetSpecification::instance();
StyleSheetProperty shorthand(
specification->getProperty( PropertyId::Transition ),
"opacity 150ms cubic-bezier(0.1, 0.2, 0.3, 0.4) 25ms, width 2s linear" );
std::vector<const StyleSheetProperty*> properties{ &shorthand };
ComputedTransitions transitions = ComputedTransitions::parse( properties );
const auto* opacity = transitions.get( String::hash( "opacity" ) );
const auto* width = transitions.get( String::hash( "width" ) );
ASSERT_TRUE( nullptr != opacity );
ASSERT_TRUE( nullptr != width );
EXPECT_EQ( opacity->duration.asMilliseconds(), 150.0 );
EXPECT_EQ( opacity->delay.asMilliseconds(), 25.0 );
EXPECT_TRUE( opacity->timingFunction == Ease::CubizBezier );
ASSERT_EQ( opacity->timingFunctionParameters.size(), 4u );
EXPECT_EQ( opacity->timingFunctionParameters[2], 0.3 );
EXPECT_EQ( width->duration.asSeconds(), 2.0 );
EXPECT_TRUE( width->timingFunction == Ease::Linear );
}
UTEST( Transitions, longhandListsCycleIndependently ) {
auto* specification = StyleSheetSpecification::instance();
StyleSheetProperty property( specification->getProperty( PropertyId::TransitionProperty ),
"opacity,width,height" );
StyleSheetProperty duration( specification->getProperty( PropertyId::TransitionDuration ),
"1s,2s" );
StyleSheetProperty delay( specification->getProperty( PropertyId::TransitionDelay ),
"10ms,20ms" );
StyleSheetProperty timing( specification->getProperty( PropertyId::TransitionTimingFunction ),
"linear,quadratic-in,bounce-out" );
std::vector<const StyleSheetProperty*> properties{ &property, &duration, &delay, &timing };
ComputedTransitions transitions = ComputedTransitions::parse( properties );
const auto* opacity = transitions.get( String::hash( "opacity" ) );
const auto* width = transitions.get( String::hash( "width" ) );
const auto* height = transitions.get( String::hash( "height" ) );
ASSERT_TRUE( nullptr != opacity );
ASSERT_TRUE( nullptr != width );
ASSERT_TRUE( nullptr != height );
EXPECT_EQ( opacity->duration.asSeconds(), 1.0 );
EXPECT_EQ( width->duration.asSeconds(), 2.0 );
EXPECT_EQ( height->duration.asSeconds(), 1.0 );
EXPECT_EQ( height->delay.asMilliseconds(), 10.0 );
EXPECT_TRUE( opacity->timingFunction == Ease::Linear );
EXPECT_TRUE( width->timingFunction == Ease::QuadraticIn );
EXPECT_TRUE( height->timingFunction == Ease::BounceOut );
TransitionsMap compatibility = TransitionDefinition::parseTransitionProperties( properties );
EXPECT_TRUE( compatibility["height"].getTimingFunction() == Ease::BounceOut );
}
UTEST( Transitions, allIsFallbackAndSpecificPropertyWins ) {
auto* specification = StyleSheetSpecification::instance();
StyleSheetProperty shorthand( specification->getProperty( PropertyId::Transition ),
"all 1s linear, opacity 2s sine-in" );
std::vector<const StyleSheetProperty*> properties{ &shorthand };
ComputedTransitions transitions = ComputedTransitions::parse( properties );
const auto* width = transitions.get( String::hash( "width" ) );
const auto* opacity = transitions.get( String::hash( "opacity" ) );
ASSERT_TRUE( nullptr != width );
ASSERT_TRUE( nullptr != opacity );
EXPECT_EQ( width->duration.asSeconds(), 1.0 );
EXPECT_EQ( opacity->duration.asSeconds(), 2.0 );
EXPECT_TRUE( opacity->timingFunction == Ease::SineIn );
}
UTEST( Transitions, allocationLightParserPreservesAcceptedSpelling ) {
auto* specification = StyleSheetSpecification::instance();
StyleSheetProperty shorthand( specification->getProperty( PropertyId::Transition ),
"opacity +150MS CUBIC-BEZIER(0.1, 0.2, 0.3, 0.4) +25MS" );
std::vector<const StyleSheetProperty*> properties{ &shorthand };
ComputedTransitions transitions = ComputedTransitions::parse( properties );
const auto* opacity = transitions.get( String::hash( "opacity" ) );
ASSERT_TRUE( nullptr != opacity );
EXPECT_EQ( opacity->duration.asMilliseconds(), 150.0 );
EXPECT_EQ( opacity->delay.asMilliseconds(), 25.0 );
EXPECT_TRUE( opacity->timingFunction == Ease::CubizBezier );
EXPECT_EQ( opacity->timingFunctionParameters.size(), 4u );
EXPECT_TRUE( TimingFunction::parse( "cubic-bezier(" ).interpolation == Ease::None );
}
@@ -60,3 +60,44 @@ UTEST( FunctionString, functionString ) {
testCase( R"(test30("str'ing", 'str"ing2'))", "test30", { "str'ing", "str\"ing2" } );
testCase( R"(test31('', ''))", "test31", { "", "" } );
}
UTEST( FunctionString, nonOwningViewParser ) {
constexpr std::string_view input =
R"( compose ( first, nested(1, "two,three"), 'quoted, value', "escaped\"quote", '' ) )";
auto function = FunctionString::parseView( input );
EXPECT_FALSE( function.isEmpty() );
EXPECT_TRUE( "compose"sv == function.getName() );
SmallVector<std::string_view, 5> parameters;
SmallVector<bool, 5> strings;
EXPECT_TRUE( function.forEachParameter( [&]( std::string_view parameter, bool wasString ) {
parameters.emplace_back( parameter );
strings.emplace_back( wasString );
return true;
} ) );
EXPECT_EQ( 5u, parameters.size() );
EXPECT_TRUE( "first"sv == parameters[0] );
EXPECT_TRUE( R"(nested(1, "two,three"))"sv == parameters[1] );
EXPECT_TRUE( "quoted, value"sv == parameters[2] );
EXPECT_TRUE( R"(escaped\"quote)"sv == parameters[3] );
EXPECT_TRUE( ""sv == parameters[4] );
EXPECT_FALSE( strings[0] );
EXPECT_FALSE( strings[1] );
EXPECT_TRUE( strings[2] );
EXPECT_TRUE( strings[3] );
EXPECT_TRUE( strings[4] );
}
UTEST( FunctionString, nonOwningViewParserCanStopEarly ) {
auto function = FunctionString::parseView( "fn(one, two, three)"sv );
int calls = 0;
std::string_view firstParameter;
EXPECT_FALSE( function.forEachParameter( [&]( std::string_view parameter, bool ) {
++calls;
firstParameter = parameter;
return false;
} ) );
EXPECT_EQ( 1, calls );
EXPECT_TRUE( "one"sv == firstParameter );
}