Reduce allocations in CSS parsing and style updates

- reuse reentrant property-resolution storage during variable substitution
  - skip redundant resolution-slot copies using structural source comparison
  - retain variable cache capacity and resolve variables in place
  - avoid copying canonical names for definition-backed properties
  - propagate resolved inherited properties by reference through widget trees
  - parse animation and transition declarations with allocation-light string views
  - share declaration tokenization between animation and transition parsers
  - avoid empty animation-map and action-query allocations
  - parse border radii without temporary split vectors
  - accept string views when converting UI lengths
  - format color hex values directly without string streams
  - use inline storage for small polygon point collections
  - preserve animation parsing semantics and fix longhand direction handling
  - save eeiv configuration when closing through keyboard shortcuts
  - extend animation, variable-resolution, property-name, and color tests
This commit is contained in:
Martín Lucas Golini
2026-08-09 19:22:00 -03:00
parent 482833d95f
commit 5f419ca58d
20 changed files with 646 additions and 332 deletions
+14 -9
View File
@@ -6,8 +6,6 @@
#include <cstdlib>
#include <cstring>
#include <ctype.h>
#include <iomanip>
#include <sstream>
#include <string_view>
namespace EE { namespace System {
@@ -611,14 +609,21 @@ Color Color::div( int divisor, bool divAlpha ) {
}
std::string Color::toHexString( const bool& prependHashtag ) const {
std::stringstream stream;
static constexpr char hex[] = "0123456789abcdef";
const std::size_t offset = prependHashtag ? 1 : 0;
std::string result( offset + ( a == 255 ? 6 : 8 ), '0' );
if ( prependHashtag )
stream << "#";
stream << std::setfill( '0' ) << std::setw( sizeof( Color ) * 2 ) << std::hex << getValue();
std::string str = stream.str();
if ( this->a == 255 )
return str.substr( 0, prependHashtag ? 7 : 6 );
return str;
result[0] = '#';
const auto writeByte = [&]( std::size_t position, Uint8 value ) {
result[offset + position] = hex[value >> 4];
result[offset + position + 1] = hex[value & 0x0F];
};
writeByte( 0, r );
writeByte( 2, g );
writeByte( 4, b );
if ( a != 255 )
writeByte( 6, a );
return result;
}
std::string Color::toRgbaString() const {
+13 -4
View File
@@ -145,11 +145,20 @@ BorderStyle Borders::toBorderStyle( const std::string& borderStyle ) {
}
Sizef Borders::radiusFromString( const UINode* node, const std::string& val ) {
auto split = String::split( val, ' ' );
std::string_view values = String::trim( std::string_view{ val }, " \t\n\r\f\v" );
const auto firstEnd = values.find_first_of( " \t\n\r\f\v" );
const std::string_view first = values.substr( 0, firstEnd );
std::string_view second = first;
if ( firstEnd != std::string_view::npos ) {
values.remove_prefix( firstEnd );
values = String::trim( values, " \t\n\r\f\v" );
const auto secondEnd = values.find_first_of( " \t\n\r\f\v" );
if ( !values.empty() )
second = values.substr( 0, secondEnd );
}
Sizef size;
size.x = node->lengthFromValue( split[0], CSS::PropertyRelativeTarget::LocalBlockRadiusWidth );
size.y = node->lengthFromValue( split[split.size() > 1 ? 1 : 0],
CSS::PropertyRelativeTarget::LocalBlockRadiusHeight );
size.x = node->lengthFromValue( first, CSS::PropertyRelativeTarget::LocalBlockRadiusWidth );
size.y = node->lengthFromValue( second, CSS::PropertyRelativeTarget::LocalBlockRadiusHeight );
return size;
}
+111 -68
View File
@@ -1,26 +1,47 @@
#include <eepp/system/functionstring.hpp>
#include <eepp/ui/css/animationdefinition.hpp>
#include <eepp/ui/css/declarationparser.hpp>
#include <eepp/ui/css/propertydefinition.hpp>
#include <eepp/ui/css/timingfunction.hpp>
namespace EE { namespace UI { namespace CSS {
inline bool isTimingFunction( const std::string& str ) {
return Ease::Interpolation::None != Ease::fromName( str, Ease::Interpolation::None );
namespace {
bool isTime( std::string_view value ) {
value = String::trim( value, " \t\n\r\f\v" );
if ( value.empty() )
return false;
auto lower = []( char character ) {
return character >= 'A' && character <= 'Z' ? character + ( 'a' - 'A' ) : character;
};
std::size_t suffixSize = 0;
if ( value.size() >= 2 && lower( value[value.size() - 2] ) == 'm' &&
lower( value.back() ) == 's' )
suffixSize = 2;
else if ( lower( value.back() ) == 's' || lower( value.back() ) == 'm' )
suffixSize = 1;
if ( 0 == suffixSize || value.size() == suffixSize )
return false;
double number = 0;
std::string_view numberView = value.substr( 0, value.size() - suffixSize );
if ( !numberView.empty() && numberView.front() == '+' )
numberView.remove_prefix( 1 );
return String::fromString( number, numberView );
}
} // namespace
UnorderedMap<std::string, AnimationDefinition> AnimationDefinition::parseAnimationProperties(
const std::vector<const StyleSheetProperty*>& stylesheetProperties ) {
AnimationsMap animations;
std::vector<std::string> names;
std::vector<Time> durations;
std::vector<Time> delays;
std::vector<Int32> iterations;
std::vector<Ease::Interpolation> timingFunctions;
std::vector<std::vector<double>> timingFunctionParameters;
std::vector<AnimationDirection> directions;
std::vector<AnimationFillMode> fillModes;
std::vector<bool> pausedStates;
SmallVector<std::string_view, 4> names;
SmallVector<Time, 4> durations;
SmallVector<Time, 4> delays;
SmallVector<Int32, 4> iterations;
SmallVector<TimingFunction, 4> timingFunctions;
SmallVector<AnimationDirection, 4> directions;
SmallVector<AnimationFillMode, 4> fillModes;
SmallVector<Uint8, 4> pausedStates;
for ( auto& prop : stylesheetProperties ) {
if ( prop->getPropertyDefinition() == NULL )
@@ -30,52 +51,49 @@ UnorderedMap<std::string, AnimationDefinition> AnimationDefinition::parseAnimati
switch ( propDef->getPropertyId() ) {
case PropertyId::Animation: {
bool durationSet = false;
for ( size_t i = 0; i < prop->getPropertyIndexCount(); i++ ) {
const StyleSheetProperty& iProp = prop->getPropertyIndex( i );
auto parts = String::split( iProp.getValue(), ' ' );
auto parts = DeclarationParser::splitWhitespaceTokens<8>( iProp.getValue() );
if ( parts.size() >= 2 ) {
AnimationDefinition animationDef;
bool durationSet = false;
for ( auto& part : parts ) {
std::string val( String::trim( String::toLower( part ) ) );
for ( std::string_view part : parts ) {
const auto hash = DeclarationParser::lowerHash( part );
if ( isDirectionString( val ) ) {
animationDef.setDirection( directionFromString( val ) );
} else if ( isAnimationFillModeString( val ) ) {
animationDef.setFillMode( fillModeFromString( val ) );
} else if ( "infinite" == val ) {
if ( isDirectionStringView( part ) ) {
animationDef.setDirection( directionFromStringView( part ) );
} else if ( isAnimationFillModeStringView( part ) ) {
animationDef.setFillMode( fillModeFromStringView( part ) );
} else if ( hash == String::hash( "infinite" ) ) {
animationDef.setIterations( -1 );
} else if ( "paused" == val ) {
} else if ( hash == String::hash( "paused" ) ) {
animationDef.setPaused( true );
} else if ( "running" == val ) {
} else if ( hash == String::hash( "running" ) ) {
animationDef.setPaused( false );
} else if ( isTimingFunction( val ) ) {
TimingFunction tf( TimingFunction::parse( val ) );
} else if ( TimingFunction tf = TimingFunction::parse( part );
Ease::Interpolation::None != tf.interpolation ) {
animationDef.setTimingFunction( tf.interpolation );
animationDef.setTimingFunctionParameters( tf.parameters );
} else if ( Time::isValid( val ) ) {
} else if ( isTime( part ) ) {
if ( durationSet ) {
animationDef.setDelay( Time::fromString( val ) );
animationDef.setDelay( DeclarationParser::parseTime( part ) );
} else {
animationDef.setDuration( Time::fromString( val ) );
animationDef.setDuration(
DeclarationParser::parseTime( part ) );
durationSet = true;
}
} else if ( String::isNumber( val, true ) ) {
int iterations = 1;
if ( String::fromString( iterations, val ) ) {
animationDef.setIterations( iterations );
}
} else if ( int iterations = 1;
String::fromString( iterations, part ) ) {
animationDef.setIterations( iterations );
} else {
animationDef.setName( part );
animationDef.setNameView( part );
}
}
animations[animationDef.getName()] = std::move( animationDef );
animations.insert_or_assign( animationDef.getName(),
std::move( animationDef ) );
}
}
return animations;
@@ -86,41 +104,44 @@ UnorderedMap<std::string, AnimationDefinition> AnimationDefinition::parseAnimati
case PropertyId::AnimationDuration:
case PropertyId::AnimationFillMode:
case PropertyId::AnimationPlayState:
case PropertyId::AnimationDirection:
case PropertyId::AnimationIterationCount:
case PropertyId::AnimationTimingFunction: {
for ( size_t i = 0; i < prop->getPropertyIndexCount(); i++ ) {
const StyleSheetProperty& iProp = prop->getPropertyIndex( i );
std::string val( String::trim( String::toLower( iProp.getValue() ) ) );
const std::string_view val =
String::trim( std::string_view{ iProp.getValue() }, " \t\n\r\f\v" );
const auto hash = DeclarationParser::lowerHash( val );
switch ( propDef->getPropertyId() ) {
case PropertyId::AnimationName:
names.push_back( iProp.getValue() );
names.emplace_back( iProp.getValue() );
break;
case PropertyId::AnimationDelay:
delays.push_back( Time::fromString( val ) );
delays.emplace_back( DeclarationParser::parseTime( val ) );
break;
case PropertyId::AnimationDuration:
durations.push_back( Time::fromString( val ) );
durations.emplace_back( DeclarationParser::parseTime( val ) );
break;
case PropertyId::AnimationFillMode:
fillModes.push_back( fillModeFromString( val ) );
fillModes.emplace_back( fillModeFromStringView( val ) );
break;
case PropertyId::AnimationPlayState:
pausedStates.push_back( val == "paused" ? true : false );
pausedStates.emplace_back( hash == String::hash( "paused" ) );
break;
case PropertyId::AnimationDirection:
directions.emplace_back( directionFromStringView( val ) );
break;
case PropertyId::AnimationIterationCount: {
int iVal;
if ( val == "infinite" ) {
iterations.push_back( -1 );
if ( hash == String::hash( "infinite" ) ) {
iterations.emplace_back( -1 );
} else if ( String::fromString( iVal, val ) && iVal >= -1 ) {
iterations.push_back( iVal );
iterations.emplace_back( iVal );
}
break;
}
case PropertyId::AnimationTimingFunction: {
TimingFunction tf( TimingFunction::parse( val ) );
timingFunctions.emplace_back( tf.interpolation );
timingFunctionParameters.emplace_back( tf.parameters.begin(),
tf.parameters.end() );
timingFunctions.emplace_back( TimingFunction::parse( val ) );
break;
}
default:
@@ -136,7 +157,7 @@ UnorderedMap<std::string, AnimationDefinition> AnimationDefinition::parseAnimati
for ( size_t i = 0; i < names.size(); i++ ) {
AnimationDefinition animationDef;
animationDef.setName( names[i] );
animationDef.setNameView( names[i] );
if ( !delays.empty() )
animationDef.setDelay( delays[i % delays.size()] );
@@ -147,35 +168,45 @@ UnorderedMap<std::string, AnimationDefinition> AnimationDefinition::parseAnimati
if ( !fillModes.empty() )
animationDef.setFillMode( fillModes[i % fillModes.size()] );
if ( !directions.empty() )
animationDef.setDirection( directions[i % directions.size()] );
if ( !pausedStates.empty() )
animationDef.setPaused( pausedStates[i % pausedStates.size()] );
if ( !iterations.empty() )
animationDef.setIterations( iterations[i % iterations.size()] );
if ( !timingFunctions.empty() )
animationDef.setTimingFunction( timingFunctions[i % timingFunctions.size()] );
if ( !timingFunctions.empty() ) {
const TimingFunction& timing = timingFunctions[i % timingFunctions.size()];
animationDef.setTimingFunction( timing.interpolation );
animationDef.setTimingFunctionParameters( timing.parameters );
}
if ( !timingFunctionParameters.empty() )
animationDef.setTimingFunctionParameters(
timingFunctionParameters[i % timingFunctionParameters.size()] );
animations[animationDef.getName()] = std::move( animationDef );
animations.insert_or_assign( animationDef.getName(), std::move( animationDef ) );
}
return animations;
}
bool AnimationDefinition::isDirectionString( const std::string str ) {
String::HashType id = String::hash( str );
return isDirectionStringView( str );
}
bool AnimationDefinition::isDirectionStringView( std::string_view str ) {
String::HashType id = DeclarationParser::lowerHash( str );
return id == AlternateReverse || id == Alternate || id == Reverse || id == Normal;
}
AnimationDefinition::AnimationDirection
AnimationDefinition::directionFromString( std::string str ) {
String::trimInPlace( str );
String::toLowerInPlace( str );
switch ( String::hash( str ) ) {
return directionFromStringView( str );
}
AnimationDefinition::AnimationDirection
AnimationDefinition::directionFromStringView( std::string_view str ) {
str = String::trim( str, " \t\n\r\f\v" );
switch ( DeclarationParser::lowerHash( str ) ) {
case AlternateReverse:
return AlternateReverse;
case Alternate:
@@ -189,14 +220,22 @@ AnimationDefinition::directionFromString( std::string str ) {
}
bool AnimationDefinition::isAnimationFillModeString( const std::string& str ) {
Uint32 id = String::hash( str );
return isAnimationFillModeStringView( str );
}
bool AnimationDefinition::isAnimationFillModeStringView( std::string_view str ) {
Uint32 id = DeclarationParser::lowerHash( str );
return id == None || id == Forwards || id == Backwards || id == Both;
}
AnimationDefinition::AnimationFillMode AnimationDefinition::fillModeFromString( std::string str ) {
String::trimInPlace( str );
String::toLowerInPlace( str );
switch ( String::hash( str ) ) {
return fillModeFromStringView( str );
}
AnimationDefinition::AnimationFillMode
AnimationDefinition::fillModeFromStringView( std::string_view str ) {
str = String::trim( str, " \t\n\r\f\v" );
switch ( DeclarationParser::lowerHash( str ) ) {
case None:
return None;
case Forwards:
@@ -240,7 +279,11 @@ const Ease::Interpolation& AnimationDefinition::getTimingFunction() const {
}
void AnimationDefinition::setName( const std::string& value ) {
mName = value;
setNameView( value );
}
void AnimationDefinition::setNameView( std::string_view value ) {
mName.assign( value );
mId = String::hash( mName );
}
+81
View File
@@ -0,0 +1,81 @@
#ifndef EE_UI_CSS_DECLARATIONPARSER_HPP
#define EE_UI_CSS_DECLARATIONPARSER_HPP
#include <cctype>
#include <eepp/core/small_vector.hpp>
#include <eepp/core/string.hpp>
#include <eepp/system/functionstring.hpp>
#include <eepp/system/time.hpp>
namespace EE { namespace UI { namespace CSS { namespace DeclarationParser {
template <typename Callback> void forEachCommaItem( std::string_view value, Callback&& callback ) {
System::FunctionString::forEachParameter( value, [&]( std::string_view item, bool ) {
callback( item );
return true;
} );
}
template <std::size_t InlineCapacity>
SmallVector<std::string_view, InlineCapacity> splitWhitespaceTokens( std::string_view value ) {
SmallVector<std::string_view, InlineCapacity> 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;
}
inline System::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 System::Time::Zero;
if ( milliseconds )
return System::Milliseconds( number );
if ( minutes )
return System::Minutes( number );
return System::Seconds( number );
}
inline String::HashType lowerHash( std::string_view value ) {
return String::hashToLower( value.data(), static_cast<Int64>( value.size() ) );
}
inline std::string lowerString( std::string_view value ) {
std::string result{ value };
String::toLowerInPlace( result );
return result;
}
}}}} // namespace EE::UI::CSS::DeclarationParser
#endif
+53 -46
View File
@@ -23,7 +23,6 @@ StyleSheetProperty::StyleSheetProperty() :
StyleSheetProperty::StyleSheetProperty( const PropertyDefinition* definition,
const std::string& value, const Uint32& index,
bool trimValue, bool cachedProperty ) :
mName( definition->getName() ),
mNameHash( definition->getId() ),
mValue( trimValue ? String::trim( value ) : value ),
mValueHash( String::hash( mValue ) ),
@@ -40,17 +39,11 @@ StyleSheetProperty::StyleSheetProperty( const PropertyDefinition* definition,
checkImportant();
createIndexed();
checkVars();
if ( NULL == mShorthandDefinition && NULL == mPropertyDefinition &&
!String::startsWith( mName, "-" ) && !isDataPropertyName( mName ) ) {
Log::warning( "Property \"%s\" is not defined!", mName );
}
}
StyleSheetProperty::StyleSheetProperty( bool isVolatile, const PropertyDefinition* definition,
const std::string& value, const Int64& /*specificity*/,
const Uint32& index ) :
mName( definition->getName() ),
mNameHash( definition->getId() ),
mValue( String::trim( value ) ),
mValueHash( String::hash( mValue ) ),
@@ -64,11 +57,6 @@ StyleSheetProperty::StyleSheetProperty( bool isVolatile, const PropertyDefinitio
cleanValue();
checkImportant();
checkVars();
if ( NULL == mShorthandDefinition && NULL == mPropertyDefinition &&
!String::startsWith( mName, "-" ) && !isDataPropertyName( mName ) ) {
Log::warning( "Property \"%s\" is not defined!", mName );
}
}
StyleSheetProperty::StyleSheetProperty( const std::string& name, const std::string& value,
@@ -140,6 +128,12 @@ ShorthandId StyleSheetProperty::getShorthandId() const {
}
const std::string& StyleSheetProperty::getName() const {
if ( !mName.empty() )
return mName;
if ( nullptr != mPropertyDefinition )
return mPropertyDefinition->getName();
if ( nullptr != mShorthandDefinition )
return mShorthandDefinition->getName();
return mName;
}
@@ -162,7 +156,7 @@ void StyleSheetProperty::setSpecificity( const Int64& specificity ) {
}
bool StyleSheetProperty::isEmpty() const {
return mName.empty();
return mName.empty() && nullptr == mPropertyDefinition && nullptr == mShorthandDefinition;
}
void StyleSheetProperty::setName( const std::string& name ) {
@@ -174,7 +168,6 @@ void StyleSheetProperty::setValue( const std::string& value, bool updateHash ) {
mValue = value;
if ( updateHash )
mValueHash = String::hash( value );
mVarCache.clear();
mIsVarValue = false;
mIsLightDarkValue = false;
createIndexed();
@@ -197,6 +190,26 @@ bool StyleSheetProperty::operator!=( const StyleSheetProperty& property ) const
return mNameHash != property.mNameHash || mValueHash != property.mValueHash;
}
bool StyleSheetProperty::hasSameResolutionSource( const StyleSheetProperty& property ) const {
if ( mName != property.mName || mNameHash != property.mNameHash || mValue != property.mValue ||
mValueHash != property.mValueHash || mSpecificity != property.mSpecificity ||
mIndex != property.mIndex || mVolatile != property.mVolatile ||
mImportant != property.mImportant || mIsVarValue != property.mIsVarValue ||
mIsLightDarkValue != property.mIsLightDarkValue ||
mCachedProperty != property.mCachedProperty ||
mPropertyDefinition != property.mPropertyDefinition ||
mShorthandDefinition != property.mShorthandDefinition ||
mIndexedProperty.size() != property.mIndexedProperty.size() )
return false;
for ( size_t i = 0; i < mIndexedProperty.size(); ++i ) {
if ( !mIndexedProperty[i].hasSameResolutionSource( property.mIndexedProperty[i] ) )
return false;
}
return true;
}
const String::HashType& StyleSheetProperty::getNameHash() const {
return mNameHash;
}
@@ -225,52 +238,46 @@ void StyleSheetProperty::createIndexed() {
}
void StyleSheetProperty::checkVars() {
auto varCache( checkVars( mValue ) );
mVarCacheSize = 0;
mIsVarValue = false;
if ( !varCache.empty() ) {
mIsVarValue = true;
mVarCache = std::move( varCache );
}
mIsLightDarkValue = mValue.find( "light-dark(" ) != std::string::npos;
}
static void varToVal( VariableFunctionCache& varCache, const std::string& varDef ) {
FunctionString functionType = FunctionString::parse( varDef );
if ( !functionType.getParameters().empty() ) {
auto varToVal = []( auto&& self, VariableFunctionCache& varCache,
std::string_view varDef ) -> void {
auto function = FunctionString::parseView( varDef );
if ( function.isEmpty() )
return;
bool foundVar = false;
for ( auto& val : functionType.getParameters() ) {
function.forEachParameter( [&]( std::string_view val, bool ) {
if ( String::startsWith( val, "--" ) ) {
varCache.variableList.emplace_back( val );
varCache.addVariable( val );
foundVar = true;
} else if ( String::startsWith( val, "var(" ) ) {
varToVal( varCache, val );
self( self, varCache, val );
foundVar = true;
} else if ( foundVar ) {
// This is a fallback value (comes after the variable name)
varCache.variableList.emplace_back( val );
varCache.addVariable( val );
}
}
}
}
return true;
} );
};
std::vector<VariableFunctionCache> StyleSheetProperty::checkVars( const std::string& value ) {
std::vector<VariableFunctionCache> vars;
std::string::size_type tokenStart = 0;
std::string::size_type tokenEnd = 0;
while ( true ) {
tokenStart = value.find( "var(", tokenStart );
tokenStart = mValue.find( "var(", tokenStart );
if ( tokenStart != std::string::npos ) {
tokenEnd = String::findCloseBracket( value, tokenStart, '(', ')' );
const std::string::size_type tokenEnd =
String::findCloseBracket( mValue, tokenStart, '(', ')' );
if ( tokenEnd != std::string::npos ) {
mIsVarValue = true;
VariableFunctionCache variableFuncCache;
variableFuncCache.definition =
value.substr( tokenStart, tokenEnd + 1 - tokenStart );
varToVal( variableFuncCache, variableFuncCache.definition );
if ( mVarCacheSize >= mVarCache.size() )
mVarCache.emplace_back();
VariableFunctionCache& variableFuncCache = mVarCache[mVarCacheSize++];
variableFuncCache.clear();
variableFuncCache.definition.assign( mValue, tokenStart,
tokenEnd + 1 - tokenStart );
varToVal( varToVal, variableFuncCache, variableFuncCache.definition );
tokenStart = tokenEnd;
vars.emplace_back( std::move( variableFuncCache ) );
} else {
break;
}
@@ -279,7 +286,7 @@ std::vector<VariableFunctionCache> StyleSheetProperty::checkVars( const std::str
}
};
return vars;
mIsLightDarkValue = mValue.find( "light-dark(" ) != std::string::npos;
}
std::string StyleSheetProperty::asString( const std::string& defaultValue ) const {
@@ -753,8 +760,8 @@ const String::HashType& StyleSheetProperty::getValueHash() const {
return mValueHash;
}
const std::vector<VariableFunctionCache>& StyleSheetProperty::getVarCache() const {
return mVarCache;
std::span<const VariableFunctionCache> StyleSheetProperty::getVarCache() const {
return { mVarCache.data(), mVarCacheSize };
}
StyleSheetProperty& StyleSheetProperty::setCachedProperty( bool cached ) {
@@ -73,7 +73,7 @@ void StyleSheetPropertyAnimation::tweenProperty(
static_cast<Int32>( startColor.a + ( endColor.a - startColor.a ) * progress ),
255 ) );
widget->applyProperty(
StyleSheetProperty( property, resColor.toHexString(), propertyIndex ) );
StyleSheetProperty( property, resColor.toHexString(), propertyIndex ) );
break;
}
case PropertyType::NumberLength: {
+4 -73
View File
@@ -1,81 +1,12 @@
#include <cctype>
#include <eepp/core/string.hpp>
#include <eepp/system/functionstring.hpp>
#include <eepp/ui/css/declarationparser.hpp>
#include <eepp/ui/css/propertydefinition.hpp>
#include <eepp/ui/css/timingfunction.hpp>
#include <eepp/ui/css/transitiondefinition.hpp>
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
using namespace DeclarationParser;
UnorderedMap<std::string, TransitionDefinition> TransitionDefinition::parseTransitionProperties(
const std::vector<const StyleSheetProperty*>& styleSheetProperties ) {
@@ -92,7 +23,7 @@ UnorderedMap<std::string, TransitionDefinition> TransitionDefinition::parseTrans
switch ( property->getPropertyDefinition()->getPropertyId() ) {
case PropertyId::Transition:
forEachCommaItem( property->getValue(), [&]( std::string_view item ) {
auto tokens = splitTransitionTokens( item );
auto tokens = splitWhitespaceTokens<4>( item );
if ( tokens.size() < 2 )
return;
TransitionDefinition transition;
@@ -192,7 +123,7 @@ ComputedTransitions::parse( const std::vector<const StyleSheetProperty*>& styleS
switch ( property->getPropertyDefinition()->getPropertyId() ) {
case PropertyId::Transition:
forEachCommaItem( property->getValue(), [&]( std::string_view item ) {
auto tokens = splitTransitionTokens( item );
auto tokens = splitWhitespaceTokens<4>( item );
if ( tokens.size() < 2 )
return;
ComputedTransitionDefinition transition;
+1 -2
View File
@@ -1678,8 +1678,7 @@ Float UINode::getPropertyRelativeTargetContainerLength(
return containerLength;
}
Float UINode::lengthFromValue( const std::string& value,
const PropertyRelativeTarget& relativeTarget,
Float UINode::lengthFromValue( std::string_view value, const PropertyRelativeTarget& relativeTarget,
const Float& defaultValue, const Uint32& propertyIndex ) const {
Float containerLength =
getPropertyRelativeTargetContainerLength( relativeTarget, defaultValue, propertyIndex );
+116 -104
View File
@@ -6,7 +6,6 @@
#include <eepp/ui/uistyle.hpp>
#include <eepp/ui/uithememanager.hpp>
#include <eepp/ui/uiwidget.hpp>
#include <optional>
using namespace EE::UI::CSS;
using namespace EE::Scene;
@@ -17,18 +16,6 @@ UIStyle* UIStyle::New( UIWidget* widget ) {
return eeNew( UIStyle, ( widget ) );
}
static const StyleSheetProperty&
resolveInheritedProperty( UIStyle* inheritedStyle, StyleSheetProperty* inheritedProperty,
std::optional<StyleSheetProperty>& resolvedProperty ) {
if ( inheritedStyle && inheritedProperty->needsValueSubstitution() ) {
resolvedProperty.emplace( *inheritedProperty );
inheritedStyle->applyVarValues( &*resolvedProperty );
return *resolvedProperty;
}
return *inheritedProperty;
}
UIStyle::UIStyle( UIWidget* widget ) :
UIState(),
mWidget( widget ),
@@ -46,6 +33,27 @@ UIStyle::~UIStyle() {
unsubscribeNonCacheableStyles();
}
UIStyle::PropertyResolution::PropertyResolution( PropertyResolution&& other ) noexcept :
mOwner( other.mOwner ), mProperty( other.mProperty ), mSlot( other.mSlot ) {
other.mOwner = nullptr;
other.mProperty = nullptr;
other.mSlot = NoSlot;
}
UIStyle::PropertyResolution::~PropertyResolution() {
release();
}
void UIStyle::PropertyResolution::release() {
if ( nullptr == mOwner || NoSlot == mSlot )
return;
eeASSERT( mOwner->mPropertyResolutionDepth == mSlot + 1 );
--mOwner->mPropertyResolutionDepth;
mOwner = nullptr;
mProperty = nullptr;
mSlot = NoSlot;
}
bool UIStyle::stateExists( const EE::Uint32& ) const {
return true;
}
@@ -118,10 +126,8 @@ void UIStyle::applyInheritedProperties() {
UIStyle* inheritedStyle = nullptr;
auto inheritedProp = getInheritedProperty( propId, &inheritedStyle );
if ( inheritedProp ) {
std::optional<StyleSheetProperty> resolvedProperty;
const auto& propertyToApply =
resolveInheritedProperty( inheritedStyle, inheritedProp, resolvedProperty );
mWidget->applyProperty( propertyToApply );
auto resolvedProperty = inheritedStyle->resolveProperty( inheritedProp );
mWidget->applyProperty( *resolvedProperty.get() );
}
}
}
@@ -140,15 +146,14 @@ bool UIStyle::hasTransition( const std::string& propertyName ) {
}
StyleSheetPropertyAnimation* UIStyle::getAnimation( const PropertyDefinition* propertyDef ) {
std::vector<Action*> actions = mWidget->getActionsByTag( propertyDef->getId() );
if ( !actions.empty() ) {
for ( auto& action : actions ) {
if ( action->getId() == StyleSheetPropertyAnimation::ID ) {
StyleSheetPropertyAnimation* animation =
static_cast<StyleSheetPropertyAnimation*>( action );
if ( animation->getAnimationOrigin() == AnimationOrigin::Animation ) {
return animation;
}
SmallVector<Action*, 4> actions;
mWidget->getActionsByTag( propertyDef->getId(), actions );
for ( auto* action : actions ) {
if ( action->getId() == StyleSheetPropertyAnimation::ID ) {
StyleSheetPropertyAnimation* animation =
static_cast<StyleSheetPropertyAnimation*>( action );
if ( animation->getAnimationOrigin() == AnimationOrigin::Animation ) {
return animation;
}
}
}
@@ -181,36 +186,46 @@ const bool& UIStyle::isChangingState() const {
}
StyleSheetVariable UIStyle::getVariable( const std::string& variable ) {
const StyleSheetVariable* resolved = getVariableRef( variable );
return resolved ? *resolved : StyleSheetVariable();
}
const StyleSheetVariable* UIStyle::getVariableRef( const std::string& variable ) {
if ( NULL != mWidget && NULL != mWidget->getUISceneNode() )
resetGlobalDefinition();
StyleSheetVariable localVariable =
mElementStyle ? mElementStyle->getVariableByName( variable ) : StyleSheetVariable();
const auto nameHash = String::hash( variable );
const StyleSheetVariable* localVariable = nullptr;
if ( mElementStyle ) {
auto it = mElementStyle->getVariables().find( nameHash );
if ( it != mElementStyle->getVariables().end() )
localVariable = &it->second;
}
if ( NULL != mGlobalDefinition ) {
auto it = mGlobalDefinition->getVariables().find( String::hash( variable ) );
auto it = mGlobalDefinition->getVariables().find( nameHash );
if ( it != mGlobalDefinition->getVariables().end() ) {
if ( localVariable.isEmpty() ||
it->second.getSpecificity() > localVariable.getSpecificity() )
return it->second;
if ( nullptr == localVariable ||
it->second.getSpecificity() > localVariable->getSpecificity() )
return &it->second;
}
}
if ( !localVariable.isEmpty() )
if ( nullptr != localVariable )
return localVariable;
Node* parentWidget = mWidget->getParentWidget();
Node* parentWidget = mWidget ? mWidget->getParentWidget() : nullptr;
if ( NULL != parentWidget ) {
UIStyle* style = parentWidget->asType<UIWidget>()->getUIStyle();
if ( NULL != style ) {
return style->getVariable( variable );
return style->getVariableRef( variable );
}
}
return StyleSheetVariable();
return nullptr;
}
bool UIStyle::getForceReapplyProperties() const {
@@ -334,22 +349,22 @@ void UIStyle::applyLightDarkValue( std::string& value ) {
};
}
void UIStyle::setVariableFromValue( StyleSheetProperty* property, const std::string& value ) {
void UIStyle::setVariableFromValue( StyleSheetProperty* property ) {
if ( property->getVarCache().empty() && !property->isLightDarkValue() )
return;
std::string newValue( value );
std::string& value = property->mutableValue();
static constexpr int maxDepth = 16;
for ( int depth = 0; depth < maxDepth; depth++ ) {
bool changed = false;
if ( !property->getVarCache().empty() ) {
const auto& varCache = property->getVarCache();
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() );
const StyleSheetVariable* variable = getVariableRef( val );
if ( nullptr != variable ) {
String::replaceAll( value, var.definition, variable->getValue() );
changed = true;
break;
}
@@ -357,12 +372,12 @@ void UIStyle::setVariableFromValue( StyleSheetProperty* property, const std::str
}
}
if ( property->isLightDarkValue() ) {
applyLightDarkValue( newValue );
applyLightDarkValue( value );
changed = true;
}
if ( !changed )
break;
property->setValue( newValue );
property->setValue( value );
if ( property->getVarCache().empty() && !property->isLightDarkValue() )
break;
}
@@ -377,10 +392,10 @@ void UIStyle::applyVarValues( StyleSheetProperty* property ) {
property->getPropertyIndexCount() > 0 ) {
for ( size_t i = 0; i < property->getPropertyIndexCount(); i++ ) {
StyleSheetProperty* realProperty = property->getPropertyIndexRef( i );
setVariableFromValue( realProperty, realProperty->getValue() );
setVariableFromValue( realProperty );
}
} else {
setVariableFromValue( property, property->getValue() );
setVariableFromValue( property );
}
}
}
@@ -438,8 +453,8 @@ void UIStyle::onStateChange() {
mWidget->beginAttributesTransaction();
for ( PropertyId prop : changedProperties ) {
std::optional<StyleSheetProperty> resolvedProperty;
StyleSheetProperty* property = getResolvedLocalProperty( prop, resolvedProperty );
auto resolvedProperty = getResolvedLocalProperty( prop );
const StyleSheetProperty* property = resolvedProperty.get();
if ( nullptr == property || NULL == property->getPropertyDefinition() ) {
const auto def = StyleSheetSpecification::instance()->getProperty( prop );
@@ -448,11 +463,10 @@ void UIStyle::onStateChange() {
StyleSheetProperty* inheritedProp =
getInheritedProperty( prop, &inheritedStyle );
if ( inheritedProp ) {
std::optional<StyleSheetProperty> resolvedInheritedProperty;
const auto& inheritedPropertyToApply = resolveInheritedProperty(
inheritedStyle, inheritedProp, resolvedInheritedProperty );
mWidget->applyProperty( inheritedPropertyToApply );
mWidget->propagateInheritedProperty( inheritedPropertyToApply );
auto resolvedInheritedProperty =
inheritedStyle->resolveProperty( inheritedProp );
mWidget->applyProperty( *resolvedInheritedProperty.get() );
mWidget->propagateInheritedProperty( *resolvedInheritedProperty.get() );
}
}
continue;
@@ -478,11 +492,10 @@ void UIStyle::onStateChange() {
mWidget->propagateInheritedProperty( resolved );
}
} else {
std::optional<StyleSheetProperty> resolvedInheritedProperty;
const auto& inheritedPropertyToApply = resolveInheritedProperty(
inheritedStyle, inheritedProp, resolvedInheritedProperty );
mWidget->applyProperty( inheritedPropertyToApply );
mWidget->propagateInheritedProperty( inheritedPropertyToApply );
auto resolvedInheritedProperty =
inheritedStyle->resolveProperty( inheritedProp );
mWidget->applyProperty( *resolvedInheritedProperty.get() );
mWidget->propagateInheritedProperty( *resolvedInheritedProperty.get() );
}
}
} else {
@@ -612,13 +625,8 @@ void UIStyle::removeRelatedWidgets() {
void UIStyle::applyStyleSheetProperty( const StyleSheetProperty& originalProperty,
std::shared_ptr<ElementDefinition> prevDefinition ) {
const StyleSheetProperty* property = &originalProperty;
std::optional<StyleSheetProperty> resolvedProperty;
if ( originalProperty.needsValueSubstitution() ) {
resolvedProperty.emplace( originalProperty );
applyVarValues( &*resolvedProperty );
property = &*resolvedProperty;
}
auto resolvedProperty = resolveProperty( &originalProperty );
const StyleSheetProperty* property = resolvedProperty.get();
const PropertyDefinition* propertyDefinition = property->getPropertyDefinition();
@@ -650,20 +658,13 @@ void UIStyle::applyStyleSheetProperty( const StyleSheetProperty& originalPropert
if ( nullptr != prevDefinition ) {
auto prevProp = prevDefinition->getProperty( property->getPropertyId() );
if ( nullptr != prevProp ) {
StyleSheetProperty* curProperty = prevProp;
std::optional<StyleSheetProperty> resolvedPrevProperty;
if ( prevProp->needsValueSubstitution() ) {
resolvedPrevProperty.emplace( *prevProp );
curProperty = &*resolvedPrevProperty;
}
auto resolvedPrevProperty = resolveProperty( prevProp );
const StyleSheetProperty* curProperty = resolvedPrevProperty.get();
if ( propertyDefinition->isIndexed() &&
property->getIndex() < curProperty->getPropertyIndexCount() ) {
StyleSheetProperty* indexedProperty =
curProperty->getPropertyIndexRef( property->getIndex() );
applyVarValues( indexedProperty );
startValue = indexedProperty->getValue();
startValue =
curProperty->getPropertyIndex( property->getIndex() ).getValue();
} else {
applyVarValues( curProperty );
startValue = curProperty->getValue();
}
}
@@ -746,24 +747,26 @@ void UIStyle::updateAnimations() {
if ( nullptr == mDefinition )
return;
bool isDifferent = false;
CSS::AnimationsMap animations;
if ( !mDefinition->getAnimationProperties().empty() ) {
animations =
AnimationDefinition::parseAnimationProperties( mDefinition->getAnimationProperties() );
if ( animations.size() == mAnimations.size() ) {
for ( auto& animation : animations ) {
auto animIt = mAnimations.find( animation.second.getName() );
if ( animIt == mAnimations.end() || animIt->second != animation.second ) {
isDifferent = true;
break;
}
}
} else {
isDifferent = true;
if ( mDefinition->getAnimationProperties().empty() ) {
if ( !mAnimations.empty() ) {
mAnimations.clear();
removeAllAnimations();
}
} else if ( !mAnimations.empty() && mDefinition->getAnimationProperties().empty() ) {
return;
}
bool isDifferent = false;
CSS::AnimationsMap animations =
AnimationDefinition::parseAnimationProperties( mDefinition->getAnimationProperties() );
if ( animations.size() == mAnimations.size() ) {
for ( auto& animation : animations ) {
auto animIt = mAnimations.find( animation.second.getName() );
if ( animIt == mAnimations.end() || animIt->second != animation.second ) {
isDifferent = true;
break;
}
}
} else {
isDifferent = true;
}
@@ -773,7 +776,7 @@ void UIStyle::updateAnimations() {
removeAllAnimations();
startAnimations( animations );
} else if ( !mDefinition->getAnimationProperties().empty() ) {
} else {
updateAnimationsPlayState();
}
}
@@ -944,16 +947,25 @@ StyleSheetProperty* UIStyle::getLocalProperty( PropertyId propId ) {
return defProperty ? defProperty : elemProperty;
}
StyleSheetProperty*
UIStyle::getResolvedLocalProperty( PropertyId propId,
std::optional<StyleSheetProperty>& resolvedProperty ) {
StyleSheetProperty* property = getLocalProperty( propId );
if ( property && property->needsValueSubstitution() ) {
resolvedProperty.emplace( *property );
applyVarValues( &*resolvedProperty );
return &*resolvedProperty;
}
return property;
UIStyle::PropertyResolution UIStyle::resolveProperty( const StyleSheetProperty* property ) {
if ( nullptr == property || !property->needsValueSubstitution() )
return PropertyResolution{ nullptr, property };
const Uint32 slotIndex = mPropertyResolutionDepth;
if ( slotIndex >= mPropertyResolutionSlots.size() )
mPropertyResolutionSlots.emplace_back( std::make_unique<StyleSheetProperty>() );
StyleSheetProperty* slot = mPropertyResolutionSlots[slotIndex].get();
++mPropertyResolutionDepth;
PropertyResolution resolution{ this, slot, slotIndex };
if ( !slot->hasSameResolutionSource( *property ) )
*slot = *property;
applyVarValues( slot );
return resolution;
}
UIStyle::PropertyResolution UIStyle::getResolvedLocalProperty( PropertyId propId ) {
return resolveProperty( getLocalProperty( propId ) );
}
void UIStyle::addStructurallyVolatileWidgetFromParent() {
+27 -13
View File
@@ -18,6 +18,7 @@
#include <eepp/ui/uiwidget.hpp>
#include <eepp/window/engine.hpp>
#include <eepp/window/window.hpp>
#include <optional>
#define PUGIXML_HEADER_ONLY
#include <pugixml/pugixml.hpp>
@@ -1994,14 +1995,18 @@ void UIWidget::setStyleSheetInlineProperty( const std::string& name, const std::
}
void UIWidget::propagateInheritedProperty( const CSS::StyleSheetProperty& property ) {
CSS::StyleSheetProperty propToPropagate = property;
const CSS::StyleSheetProperty* propertyToPropagate = &property;
std::optional<CSS::StyleSheetProperty> resolvedProperty;
if ( propToPropagate.needsValueSubstitution() && NULL != mStyle )
mStyle->applyVarValues( &propToPropagate );
if ( property.needsValueSubstitution() && NULL != mStyle ) {
resolvedProperty.emplace( property );
mStyle->applyVarValues( &*resolvedProperty );
propertyToPropagate = &*resolvedProperty;
}
if ( propToPropagate.getPropertyDefinition() &&
propToPropagate.getPropertyDefinition()->getPropertyId() == PropertyId::FontSize ) {
StyleSheetLength length( propToPropagate.value() );
if ( propertyToPropagate->getPropertyDefinition() &&
propertyToPropagate->getPropertyDefinition()->getPropertyId() == PropertyId::FontSize ) {
StyleSheetLength length( propertyToPropagate->value() );
Float pxSize = 0;
if ( length.getUnit() == StyleSheetLength::Unit::Rem ) {
@@ -2029,14 +2034,23 @@ void UIWidget::propagateInheritedProperty( const CSS::StyleSheetProperty& proper
else
pxSize = ( length.getValue() / 100.f ) * parentFontSize;
} else {
pxSize = lengthFromValue( propToPropagate );
pxSize = lengthFromValue( *propertyToPropagate );
}
propToPropagate = CSS::StyleSheetProperty(
propToPropagate.getName(), String::fromFloat( PixelDensity::pxToDp( pxSize ), "dp" ),
propToPropagate.getSpecificity() );
const PropertyDefinition* propertyDefinition = propertyToPropagate->getPropertyDefinition();
const Uint32 propertyIndex = propertyToPropagate->getIndex();
const Int64 specificity = propertyToPropagate->getSpecificity();
resolvedProperty.emplace( propertyDefinition,
String::fromFloat( PixelDensity::pxToDp( pxSize ), "dp" ),
propertyIndex );
resolvedProperty->setSpecificity( specificity );
propertyToPropagate = &*resolvedProperty;
}
propagateInheritedPropertyResolved( *propertyToPropagate );
}
void UIWidget::propagateInheritedPropertyResolved( const CSS::StyleSheetProperty& property ) {
Node* child = getFirstChild();
while ( child ) {
if ( child->isWidget() ) {
@@ -2044,9 +2058,9 @@ void UIWidget::propagateInheritedProperty( const CSS::StyleSheetProperty& proper
UIStyle* childStyle = childWidget->getUIStyle();
// Only propagate if the child doesn't explicitly override it
if ( childStyle && !childStyle->hasLocalProperty(
propToPropagate.getPropertyDefinition()->getPropertyId() ) ) {
childWidget->applyProperty( propToPropagate );
childWidget->propagateInheritedProperty( propToPropagate );
property.getPropertyDefinition()->getPropertyId() ) ) {
childWidget->applyProperty( property );
childWidget->propagateInheritedPropertyResolved( property );
}
}
child = child->getNextNode();
@@ -1,6 +1,7 @@
#include "utest.h"
#include <eepp/scene/actions/delay.hpp>
#include <eepp/scene/actions/runnable.hpp>
#include <eepp/ui/css/animationdefinition.hpp>
#include <eepp/ui/css/stylesheetspecification.hpp>
#include <eepp/ui/css/transitiondefinition.hpp>
@@ -123,3 +124,57 @@ UTEST( Transitions, allocationLightParserPreservesAcceptedSpelling ) {
EXPECT_EQ( opacity->timingFunctionParameters.size(), 4u );
EXPECT_TRUE( TimingFunction::parse( "cubic-bezier(" ).interpolation == Ease::None );
}
UTEST( Animations, allocationLightShorthandPreservesSemantics ) {
auto* specification = StyleSheetSpecification::instance();
StyleSheetProperty shorthand(
specification->getProperty( PropertyId::Animation ),
"fade +150MS CUBIC-BEZIER(0.1, 0.2, 0.3, 0.4) +25MS 3 alternate both paused, "
"slide 2s linear" );
std::vector<const StyleSheetProperty*> properties{ &shorthand };
AnimationsMap animations = AnimationDefinition::parseAnimationProperties( properties );
auto fade = animations.find( "fade" );
auto slide = animations.find( "slide" );
ASSERT_TRUE( fade != animations.end() );
ASSERT_TRUE( slide != animations.end() );
EXPECT_EQ( fade->second.getDuration().asMilliseconds(), 150.0 );
EXPECT_EQ( fade->second.getDelay().asMilliseconds(), 25.0 );
EXPECT_EQ( fade->second.getIterations(), 3 );
EXPECT_TRUE( fade->second.getDirection() == AnimationDefinition::Alternate );
EXPECT_TRUE( fade->second.getFillMode() == AnimationDefinition::Both );
EXPECT_TRUE( fade->second.isPaused() );
EXPECT_TRUE( fade->second.getTimingFunction() == Ease::CubizBezier );
EXPECT_EQ( fade->second.getTimingFunctionParametersInline().size(), 4u );
EXPECT_EQ( slide->second.getDuration().asSeconds(), 2.0 );
EXPECT_EQ( slide->second.getDelay().asSeconds(), 0.0 );
}
UTEST( Animations, allocationLightLonghandListsCycleIndependently ) {
auto* specification = StyleSheetSpecification::instance();
StyleSheetProperty names( specification->getProperty( PropertyId::AnimationName ),
"first,second,third" );
StyleSheetProperty durations( specification->getProperty( PropertyId::AnimationDuration ),
"1s,2s" );
StyleSheetProperty delays( specification->getProperty( PropertyId::AnimationDelay ),
"10ms,20ms" );
StyleSheetProperty directions( specification->getProperty( PropertyId::AnimationDirection ),
"reverse,alternate" );
StyleSheetProperty timing( specification->getProperty( PropertyId::AnimationTimingFunction ),
"linear,cubic-bezier(0.1, 0.2, 0.3, 0.4)" );
std::vector<const StyleSheetProperty*> properties{ &names, &durations, &delays, &directions,
&timing };
AnimationsMap animations = AnimationDefinition::parseAnimationProperties( properties );
ASSERT_EQ( animations.size(), 3u );
EXPECT_EQ( animations.at( "first" ).getDuration().asSeconds(), 1.0 );
EXPECT_EQ( animations.at( "second" ).getDuration().asSeconds(), 2.0 );
EXPECT_EQ( animations.at( "third" ).getDuration().asSeconds(), 1.0 );
EXPECT_EQ( animations.at( "third" ).getDelay().asMilliseconds(), 10.0 );
EXPECT_TRUE( animations.at( "first" ).getDirection() == AnimationDefinition::Reverse );
EXPECT_TRUE( animations.at( "second" ).getDirection() == AnimationDefinition::Alternate );
EXPECT_TRUE( animations.at( "third" ).getDirection() == AnimationDefinition::Reverse );
EXPECT_TRUE( animations.at( "first" ).getTimingFunction() == Ease::Linear );
EXPECT_TRUE( animations.at( "second" ).getTimingFunction() == Ease::CubizBezier );
EXPECT_EQ( animations.at( "second" ).getTimingFunctionParametersInline().size(), 4u );
}
@@ -280,3 +280,10 @@ UTEST( ColorParsing, hsvaWithThreeLegacy ) {
EXPECT_COLOREQ( Color( 255, 0, 0, 255 ),
Color::fromString( "hsva(0, 100%, 100%)" ) );
}
UTEST( ColorParsing, hexSerialization ) {
EXPECT_STDSTREQ( "#01020f", Color( 1, 2, 15 ).toHexString() );
EXPECT_STDSTREQ( "01020f", Color( 1, 2, 15 ).toHexString( false ) );
EXPECT_STDSTREQ( "#01020f10", Color( 1, 2, 15, 16 ).toHexString() );
EXPECT_STDSTREQ( "01020f10", Color( 1, 2, 15, 16 ).toHexString( false ) );
}
@@ -31,6 +31,44 @@ using namespace EE::UI::CSS;
using namespace EE::Scene;
using namespace EE::Graphics;
namespace {
class ResolutionStorageTestStyle : public UIStyle {
public:
ResolutionStorageTestStyle() : UIStyle( nullptr ) {}
struct Probe {
const StyleSheetProperty* firstAddress{};
const StyleSheetProperty* nestedAddress{};
const StyleSheetProperty* reusedAddress{};
std::string firstValueBeforeNested;
std::string firstValueAfterNested;
std::string nestedValue;
};
Probe probe( const StyleSheetProperty& first, const StyleSheetProperty& nested ) {
Probe probe;
{
auto firstResolution = resolveProperty( &first );
probe.firstAddress = firstResolution.get();
probe.firstValueBeforeNested = firstResolution.get()->getValue();
{
auto nestedResolution = resolveProperty( &nested );
probe.nestedAddress = nestedResolution.get();
probe.nestedValue = nestedResolution.get()->getValue();
}
probe.firstValueAfterNested = firstResolution.get()->getValue();
}
{
auto reusedResolution = resolveProperty( &first );
probe.reusedAddress = reusedResolution.get();
}
return probe;
}
};
} // namespace
UTEST( CSSParser, UnknownPropertiesKeepDistinctNameHashes ) {
StyleSheetProperty first( "data-first", "one" );
StyleSheetProperty second( "data-second", "two" );
@@ -44,6 +82,17 @@ UTEST( CSSParser, UnknownPropertiesKeepDistinctNameHashes ) {
EXPECT_TRUE( first.getShorthandId() == ShorthandId::Invalid );
}
UTEST( CSSParser, DefinitionBackedPropertiesUseCanonicalDefinitionName ) {
auto* definition =
StyleSheetSpecification::instance()->getProperty( PropertyId::BackgroundColor );
ASSERT_TRUE( nullptr != definition );
StyleSheetProperty property( definition, "#010203" );
EXPECT_FALSE( property.isEmpty() );
EXPECT_STDSTREQ( definition->getName(), property.getName() );
EXPECT_EQ( definition->getId(), property.getNameHash() );
}
UTEST( CSSParser, CommentsPreserveDeclarationContext ) {
StyleSheetPropertiesParser parser( R"css(
color /* before colon */ : red;
@@ -487,6 +536,40 @@ UTEST( CSSVariables, StyleSheetPropertyNeedsValueSubstitution ) {
.needsValueSubstitution() );
}
UTEST( CSSVariables, ResolutionStorageIsReentrantAndReusable ) {
ResolutionStorageTestStyle style;
style.setStyleSheetVariable( StyleSheetVariable( "--first", "#123456" ) );
style.setStyleSheetVariable( StyleSheetVariable( "--nested", "#ABCDEF" ) );
const auto probe = style.probe( StyleSheetProperty( "color", "var(--first)" ),
StyleSheetProperty( "background-color", "var(--nested)" ) );
EXPECT_TRUE( nullptr != probe.firstAddress );
EXPECT_TRUE( nullptr != probe.nestedAddress );
EXPECT_NE( probe.firstAddress, probe.nestedAddress );
EXPECT_EQ( probe.firstAddress, probe.reusedAddress );
EXPECT_STDSTREQ( "#123456", probe.firstValueBeforeNested );
EXPECT_STDSTREQ( "#123456", probe.firstValueAfterNested );
EXPECT_STDSTREQ( "#ABCDEF", probe.nestedValue );
}
UTEST( CSSVariables, StyleSheetPropertyReusesVariableCacheStorage ) {
StyleSheetProperty property( "color", "var(--a-variable-name-long-enough-to-allocate)" );
auto firstCache = property.getVarCache();
ASSERT_EQ( 1u, firstCache.size() );
const auto* cacheAddress = &*firstCache.begin();
const auto* variableStorage = firstCache.begin()->variableList.data();
property.setValue( "#123456" );
EXPECT_TRUE( property.getVarCache().empty() );
property.setValue( "var(--a-variable-name-long-enough-to-allocate)" );
auto reusedCache = property.getVarCache();
ASSERT_EQ( 1u, reusedCache.size() );
EXPECT_EQ( cacheAddress, &*reusedCache.begin() );
EXPECT_EQ( variableStorage, reusedCache.begin()->variableList.data() );
}
UTEST( CSSVariables, StyleAttributeVarOnRichTextAndTextSpan ) {
UIApplication app(
WindowSettings( 800, 600, "eepp - CSS Style Attribute Var Test", WindowStyle::Default,
+6 -3
View File
@@ -281,8 +281,10 @@ bool App::init() {
mConsole->setMaxLogLines( 1024000 );
mConsole->setFontSize( mConfig.ConsoleFontSize );
mConsole->on( Event::KeyDown, [this]( const Event* event ) {
if ( event->asKeyEvent()->getKeyCode() == KEY_ESCAPE )
if ( event->asKeyEvent()->getKeyCode() == KEY_ESCAPE ) {
saveConfig();
mUIApplication->getWindow()->close();
}
} );
mConsoleCommands = std::make_unique<ConsoleCommands>( this, mConsole );
registerConsoleCommands();
@@ -357,9 +359,10 @@ void App::registerKeyBindings() {
}
if ( mConsole->isActive() )
return;
if ( key == KEY_ESCAPE || key == KEY_Q )
if ( key == KEY_ESCAPE || key == KEY_Q ) {
saveConfig();
getWindow()->close();
else if ( key == KEY_TAB && keyEvent->getMod() & KEYMOD_ALT )
} else if ( key == KEY_TAB && keyEvent->getMod() & KEYMOD_ALT )
getWindow()->minimize();
else if ( key == KEY_F || ( key == KEY_RETURN && keyEvent->getMod() & KEYMOD_ALT ) )
getWindow()->toggleFullscreen();