diff --git a/.ecode/project_build.json b/.ecode/project_build.json index 185197348..235e452bc 100644 --- a/.ecode/project_build.json +++ b/.ecode/project_build.json @@ -377,7 +377,7 @@ "working_dir": "${project_root}/bin" }, { - "args": "-c system --hn-dark", + "args": "https://www.0xsid.com/blog/wont-download-your-app", "command": "${project_root}/bin/eepp-ui-html-debug", "name": "eepp-ui-html-debug", "working_dir": "${project_root}/bin" diff --git a/include/eepp/graphics/drawable.hpp b/include/eepp/graphics/drawable.hpp index 23d89c40a..2b5e4cfd8 100644 --- a/include/eepp/graphics/drawable.hpp +++ b/include/eepp/graphics/drawable.hpp @@ -33,6 +33,9 @@ class EE_API Drawable { UIBACKGROUNDDRAWABLE, RICHTEXT, LINEARGRADIENT, + REPEATINGLINEARGRADIENT, + RADIALGRADIENT, + REPEATINGRADIALGRADIENT, CUSTOM }; diff --git a/include/eepp/graphics/lineargradientdrawable.hpp b/include/eepp/graphics/lineargradientdrawable.hpp index 5a72808a6..0902b9a96 100644 --- a/include/eepp/graphics/lineargradientdrawable.hpp +++ b/include/eepp/graphics/lineargradientdrawable.hpp @@ -18,8 +18,9 @@ class EE_API LinearGradientDrawable : public Drawable { }; static LinearGradientDrawable* New(); + static LinearGradientDrawable* NewRepeating(); - LinearGradientDrawable(); + LinearGradientDrawable( Drawable::Type drawableType = LINEARGRADIENT ); virtual Sizef getSize(); @@ -43,6 +44,8 @@ class EE_API LinearGradientDrawable : public Drawable { void setSize( const Sizef& size ); + bool isRepeating() const; + protected: std::vector mColorStops; Float mAngle{ 180.f }; /* CSS angle: 0=to top, 90=to right, 180=to bottom (default) */ diff --git a/include/eepp/graphics/radialgradientdrawable.hpp b/include/eepp/graphics/radialgradientdrawable.hpp new file mode 100644 index 000000000..dcadf4af9 --- /dev/null +++ b/include/eepp/graphics/radialgradientdrawable.hpp @@ -0,0 +1,70 @@ +#ifndef EE_GRAPHICS_RADIALGRADIENTDRAWABLE_HPP +#define EE_GRAPHICS_RADIALGRADIENTDRAWABLE_HPP + +#include +#include +#include + +namespace EE { namespace Graphics { + +class EE_API RadialGradientDrawable : public Drawable { + public: + struct ColorStop { + Float position{ 0 }; + Color color; + + ColorStop() : color( Color::White ) {} + ColorStop( Float pos, const Color& col ) : position( pos ), color( col ) {} + }; + + enum ShapeType { CIRCLE, ELLIPSE }; + enum Extent { CLOSEST_SIDE, FARTHEST_SIDE, CLOSEST_CORNER, FARTHEST_CORNER }; + + static RadialGradientDrawable* New(); + static RadialGradientDrawable* NewRepeating(); + + RadialGradientDrawable( Drawable::Type drawableType = RADIALGRADIENT ); + + virtual Sizef getSize(); + + virtual Sizef getPixelsSize(); + + virtual void draw(); + + virtual void draw( const Vector2f& position ); + + virtual void draw( const Vector2f& position, const Sizef& size ); + + virtual bool isStateful() { return false; } + + const std::vector& getColorStops() const; + + void setColorStops( std::vector stops ); + + ShapeType getShape() const; + + void setShape( ShapeType shape ); + + Extent getExtent() const; + + void setExtent( Extent extent ); + + const Vector2f& getCenter() const; + + void setCenter( const Vector2f& centerNormalized ); + + void setSize( const Sizef& size ); + + bool isRepeating() const; + + protected: + std::vector mColorStops; + ShapeType mShape{ CIRCLE }; + Extent mExtent{ FARTHEST_CORNER }; + Vector2f mCenter{ 0.5f, 0.5f }; + Sizef mSize; +}; + +}} // namespace EE::Graphics + +#endif diff --git a/src/eepp/graphics/lineargradientdrawable.cpp b/src/eepp/graphics/lineargradientdrawable.cpp index a2cdb1d49..6c9009b94 100644 --- a/src/eepp/graphics/lineargradientdrawable.cpp +++ b/src/eepp/graphics/lineargradientdrawable.cpp @@ -10,7 +10,12 @@ LinearGradientDrawable* LinearGradientDrawable::New() { return eeNew( LinearGradientDrawable, () ); } -LinearGradientDrawable::LinearGradientDrawable() : Drawable( LINEARGRADIENT ) {} +LinearGradientDrawable* LinearGradientDrawable::NewRepeating() { + return eeNew( LinearGradientDrawable, ( REPEATINGLINEARGRADIENT ) ); +} + +LinearGradientDrawable::LinearGradientDrawable( Drawable::Type drawableType ) : + Drawable( drawableType ) {} Sizef LinearGradientDrawable::getSize() { return mSize; @@ -51,7 +56,6 @@ void LinearGradientDrawable::draw( const Vector2f& position, const Sizef& size ) auto addT = [&]( Float t, Float px, Float py ) { if ( !std::isfinite( t ) ) return; - // Clamp check point to box if ( px >= -0.001f && px <= w + 0.001f && py >= -0.001f && py <= h + 0.001f ) { tVals[validCount++] = t; } @@ -85,6 +89,8 @@ void LinearGradientDrawable::draw( const Vector2f& position, const Sizef& size ) if ( std::abs( tMax - tMin ) < 0.0001f ) return; + Float txLen = tMax - tMin; + // Perpendicular direction (rotated 90° clockwise in screen coords) Float px = -dy; Float py = dx; @@ -94,81 +100,210 @@ void LinearGradientDrawable::draw( const Vector2f& position, const Sizef& size ) sBR->setBlendMode( BlendMode::Alpha() ); sBR->quadsBegin(); - // Pre-compute position on gradient line: center + t * dir - Float txLen = tMax - tMin; - - // Pre-compute perpendicular intersections for all stops - struct PerpSeg { - Vector2f a; - Vector2f b; - Float gradPos; // t value along gradient line - }; - std::vector segments; - segments.reserve( mColorStops.size() ); - - for ( size_t i = 0; i < mColorStops.size(); i++ ) { - PerpSeg seg; - Float t = tMin + mColorStops[i].position * txLen; - seg.gradPos = t; - Float gx = cx + t * dx; - Float gy = cy + t * dy; - - // Find perpendicular line intersections with the box - Float ptVals[4]; - int ptCount = 0; - - auto addPT = [&]( Float pt, Float qx, Float qy ) { - if ( !std::isfinite( pt ) ) - return; - if ( qx >= -0.001f && qx <= w + 0.001f && qy >= -0.001f && qy <= h + 0.001f ) { - ptVals[ptCount++] = pt; - } - }; - - if ( std::abs( px ) > 0.0001f ) { - Float ptLeft = -gx / px; - addPT( ptLeft, 0, gy + ptLeft * py ); - Float ptRight = ( w - gx ) / px; - addPT( ptRight, w, gy + ptRight * py ); - } - if ( std::abs( py ) > 0.0001f ) { - Float ptTop = -gy / py; - addPT( ptTop, gx + ptTop * px, 0 ); - Float ptBottom = ( h - gy ) / py; - addPT( ptBottom, gx + ptBottom * px, h ); - } - - if ( ptCount < 2 ) + if ( isRepeating() ) { + Float firstPos = mColorStops.front().position; + Float lastPos = mColorStops.back().position; + Float patternLen = lastPos - firstPos; + if ( patternLen < 0.0001f ) { + sBR->draw(); return; - - Float ptMin = ptVals[0]; - Float ptMax = ptVals[0]; - for ( int j = 1; j < ptCount; j++ ) { - if ( ptVals[j] < ptMin ) - ptMin = ptVals[j]; - if ( ptVals[j] > ptMax ) - ptMax = ptVals[j]; } - seg.a = Vector2f( gx + ptMin * px + position.x, gy + ptMin * py + position.y ); - seg.b = Vector2f( gx + ptMax * px + position.x, gy + ptMax * py + position.y ); - segments.push_back( seg ); - } + // Visible position range mapped to [0, 1] along the gradient-line + // segment visible in the box. + // Pattern start in position space: firstPos + // Repeat interval: patternLen + // Visible range: [0, 1] + int rStart = (int)std::floor( ( -firstPos ) / patternLen ); + int rEnd = (int)std::ceil( ( 1.f - lastPos ) / patternLen ); + const int MAX_REPEATS = 128; + if ( rEnd - rStart > MAX_REPEATS ) + rEnd = rStart + MAX_REPEATS; - // Render quads between consecutive stops - for ( size_t i = 0; i < mColorStops.size() - 1; i++ ) { - const Color& c0 = mColorStops[i].color; - const Color& c1 = mColorStops[i + 1].color; + for ( int r = rStart; r <= rEnd; r++ ) { + Float repeatOff = (Float)r * patternLen; - Color fc0 = ( mColor.a == 255 ) ? c0 : Color( c0 ).blendAlpha( mColor.a ); - Color fc1 = ( mColor.a == 255 ) ? c1 : Color( c1 ).blendAlpha( mColor.a ); + for ( size_t i = 0; i + 1 < mColorStops.size(); i++ ) { + Float p0 = mColorStops[i].position + repeatOff; + Float p1 = mColorStops[i + 1].position + repeatOff; - sBR->quadsSetColorFree( fc0, fc1, fc1, fc0 ); + if ( p1 <= 0.f || p0 >= 1.f ) + continue; - const PerpSeg& s0 = segments[i]; - const PerpSeg& s1 = segments[i + 1]; + Float clip0 = eemax( 0.f, p0 ); + Float clip1 = eemin( 1.f, p1 ); + if ( clip1 - clip0 < 0.0001f ) + continue; - sBR->batchQuadFree( s0.a.x, s0.a.y, s1.a.x, s1.a.y, s1.b.x, s1.b.y, s0.b.x, s0.b.y ); + // Colors at clipped band boundaries (interpolate within the stop pair) + Float bw = p1 - p0; + Float frac0 = ( clip0 - p0 ) / bw; + Float frac1 = ( clip1 - p0 ) / bw; + const Color& sc0 = mColorStops[i].color; + const Color& sc1 = mColorStops[i + 1].color; + Color cc0( (Uint8)( (Float)sc0.r + frac0 * (Float)( sc1.r - sc0.r ) ), + (Uint8)( (Float)sc0.g + frac0 * (Float)( sc1.g - sc0.g ) ), + (Uint8)( (Float)sc0.b + frac0 * (Float)( sc1.b - sc0.b ) ), + (Uint8)( (Float)sc0.a + frac0 * (Float)( sc1.a - sc0.a ) ) ); + Color cc1( (Uint8)( (Float)sc0.r + frac1 * (Float)( sc1.r - sc0.r ) ), + (Uint8)( (Float)sc0.g + frac1 * (Float)( sc1.g - sc0.g ) ), + (Uint8)( (Float)sc0.b + frac1 * (Float)( sc1.b - sc0.b ) ), + (Uint8)( (Float)sc0.a + frac1 * (Float)( sc1.a - sc0.a ) ) ); + + Float tc0 = tMin + clip0 * txLen; + Float tc1 = tMin + clip1 * txLen; + Float gx0 = cx + tc0 * dx; + Float gy0 = cy + tc0 * dy; + Float gx1 = cx + tc1 * dx; + Float gy1 = cy + tc1 * dy; + + // Perpendicular intersection for point 0 + Float ptVals0[4]; + int ptCount0 = 0; + auto addPT0 = [&]( Float pt, Float qx, Float qy ) { + if ( !std::isfinite( pt ) ) + return; + if ( qx >= -0.001f && qx <= w + 0.001f && qy >= -0.001f && qy <= h + 0.001f ) + ptVals0[ptCount0++] = pt; + }; + if ( std::abs( px ) > 0.0001f ) { + Float ptLeft = -gx0 / px; + addPT0( ptLeft, 0, gy0 + ptLeft * py ); + Float ptRight = ( w - gx0 ) / px; + addPT0( ptRight, w, gy0 + ptRight * py ); + } + if ( std::abs( py ) > 0.0001f ) { + Float ptTop = -gy0 / py; + addPT0( ptTop, gx0 + ptTop * px, 0 ); + Float ptBottom = ( h - gy0 ) / py; + addPT0( ptBottom, gx0 + ptBottom * px, h ); + } + if ( ptCount0 < 2 ) + continue; + Float ptMin0 = ptVals0[0], ptMax0 = ptVals0[0]; + for ( int j = 1; j < ptCount0; j++ ) { + if ( ptVals0[j] < ptMin0 ) + ptMin0 = ptVals0[j]; + if ( ptVals0[j] > ptMax0 ) + ptMax0 = ptVals0[j]; + } + Vector2f a0( gx0 + ptMin0 * px + position.x, gy0 + ptMin0 * py + position.y ); + Vector2f b0( gx0 + ptMax0 * px + position.x, gy0 + ptMax0 * py + position.y ); + + // Perpendicular intersection for point 1 + Float ptVals1[4]; + int ptCount1 = 0; + auto addPT1 = [&]( Float pt, Float qx, Float qy ) { + if ( !std::isfinite( pt ) ) + return; + if ( qx >= -0.001f && qx <= w + 0.001f && qy >= -0.001f && qy <= h + 0.001f ) + ptVals1[ptCount1++] = pt; + }; + if ( std::abs( px ) > 0.0001f ) { + Float ptLeft = -gx1 / px; + addPT1( ptLeft, 0, gy1 + ptLeft * py ); + Float ptRight = ( w - gx1 ) / px; + addPT1( ptRight, w, gy1 + ptRight * py ); + } + if ( std::abs( py ) > 0.0001f ) { + Float ptTop = -gy1 / py; + addPT1( ptTop, gx1 + ptTop * px, 0 ); + Float ptBottom = ( h - gy1 ) / py; + addPT1( ptBottom, gx1 + ptBottom * px, h ); + } + if ( ptCount1 < 2 ) + continue; + Float ptMin1 = ptVals1[0], ptMax1 = ptVals1[0]; + for ( int j = 1; j < ptCount1; j++ ) { + if ( ptVals1[j] < ptMin1 ) + ptMin1 = ptVals1[j]; + if ( ptVals1[j] > ptMax1 ) + ptMax1 = ptVals1[j]; + } + Vector2f a1( gx1 + ptMin1 * px + position.x, gy1 + ptMin1 * py + position.y ); + Vector2f b1( gx1 + ptMax1 * px + position.x, gy1 + ptMax1 * py + position.y ); + + Color fc0 = ( mColor.a == 255 ) ? cc0 : Color( cc0 ).blendAlpha( mColor.a ); + Color fc1 = ( mColor.a == 255 ) ? cc1 : Color( cc1 ).blendAlpha( mColor.a ); + sBR->quadsSetColorFree( fc0, fc1, fc1, fc0 ); + sBR->batchQuadFree( a0.x, a0.y, a1.x, a1.y, b1.x, b1.y, b0.x, b0.y ); + } + } + } else { + // Pre-compute position on gradient line: center + t * dir + // Pre-compute perpendicular intersections for all stops + struct PerpSeg { + Vector2f a; + Vector2f b; + Float gradPos; + }; + std::vector segments; + segments.reserve( mColorStops.size() ); + + for ( size_t i = 0; i < mColorStops.size(); i++ ) { + PerpSeg seg; + Float t = tMin + mColorStops[i].position * txLen; + seg.gradPos = t; + Float gx = cx + t * dx; + Float gy = cy + t * dy; + + // Find perpendicular line intersections with the box + Float ptVals[4]; + int ptCount = 0; + + auto addPT = [&]( Float pt, Float qx, Float qy ) { + if ( !std::isfinite( pt ) ) + return; + if ( qx >= -0.001f && qx <= w + 0.001f && qy >= -0.001f && qy <= h + 0.001f ) { + ptVals[ptCount++] = pt; + } + }; + + if ( std::abs( px ) > 0.0001f ) { + Float ptLeft = -gx / px; + addPT( ptLeft, 0, gy + ptLeft * py ); + Float ptRight = ( w - gx ) / px; + addPT( ptRight, w, gy + ptRight * py ); + } + if ( std::abs( py ) > 0.0001f ) { + Float ptTop = -gy / py; + addPT( ptTop, gx + ptTop * px, 0 ); + Float ptBottom = ( h - gy ) / py; + addPT( ptBottom, gx + ptBottom * px, h ); + } + + if ( ptCount < 2 ) + return; + + Float ptMin = ptVals[0]; + Float ptMax = ptVals[0]; + for ( int j = 1; j < ptCount; j++ ) { + if ( ptVals[j] < ptMin ) + ptMin = ptVals[j]; + if ( ptVals[j] > ptMax ) + ptMax = ptVals[j]; + } + + seg.a = Vector2f( gx + ptMin * px + position.x, gy + ptMin * py + position.y ); + seg.b = Vector2f( gx + ptMax * px + position.x, gy + ptMax * py + position.y ); + segments.push_back( seg ); + } + + // Render quads between consecutive stops + for ( size_t i = 0; i < mColorStops.size() - 1; i++ ) { + const Color& c0 = mColorStops[i].color; + const Color& c1 = mColorStops[i + 1].color; + + Color fc0 = ( mColor.a == 255 ) ? c0 : Color( c0 ).blendAlpha( mColor.a ); + Color fc1 = ( mColor.a == 255 ) ? c1 : Color( c1 ).blendAlpha( mColor.a ); + + sBR->quadsSetColorFree( fc0, fc1, fc1, fc0 ); + + const PerpSeg& s0 = segments[i]; + const PerpSeg& s1 = segments[i + 1]; + + sBR->batchQuadFree( s0.a.x, s0.a.y, s1.a.x, s1.a.y, s1.b.x, s1.b.y, s0.b.x, s0.b.y ); + } } sBR->draw(); @@ -185,11 +320,17 @@ void LinearGradientDrawable::setColorStops( std::vector stops ) { std::sort( mColorStops.begin(), mColorStops.end(), []( const ColorStop& a, const ColorStop& b ) { return a.position < b.position; } ); - mColorStops.front().position = 0.f; - mColorStops.back().position = 1.f; + if ( !isRepeating() ) { + mColorStops.front().position = 0.f; + mColorStops.back().position = 1.f; + } } } +bool LinearGradientDrawable::isRepeating() const { + return mDrawableType == REPEATINGLINEARGRADIENT; +} + Float LinearGradientDrawable::getAngle() const { return mAngle; } diff --git a/src/eepp/graphics/radialgradientdrawable.cpp b/src/eepp/graphics/radialgradientdrawable.cpp new file mode 100644 index 000000000..9d3e04f61 --- /dev/null +++ b/src/eepp/graphics/radialgradientdrawable.cpp @@ -0,0 +1,225 @@ +#include +#include + +#include +#include +#include + +namespace EE { namespace Graphics { + +RadialGradientDrawable* RadialGradientDrawable::New() { + return eeNew( RadialGradientDrawable, () ); +} + +RadialGradientDrawable* RadialGradientDrawable::NewRepeating() { + return eeNew( RadialGradientDrawable, ( REPEATINGRADIALGRADIENT ) ); +} + +RadialGradientDrawable::RadialGradientDrawable( Drawable::Type drawableType ) : + Drawable( drawableType ) {} + +Sizef RadialGradientDrawable::getSize() { + return mSize; +} + +Sizef RadialGradientDrawable::getPixelsSize() { + return mSize; +} + +void RadialGradientDrawable::draw() { + draw( mPosition, mSize ); +} + +void RadialGradientDrawable::draw( const Vector2f& position ) { + draw( position, mSize ); +} + +static Float computeRadius( Float cx, Float cy, Float w, Float h, + RadialGradientDrawable::Extent extent ) { + Float dTL = std::sqrt( cx * cx + cy * cy ); + Float dTR = std::sqrt( ( w - cx ) * ( w - cx ) + cy * cy ); + Float dBL = std::sqrt( cx * cx + ( h - cy ) * ( h - cy ) ); + Float dBR = std::sqrt( ( w - cx ) * ( w - cx ) + ( h - cy ) * ( h - cy ) ); + + switch ( extent ) { + case RadialGradientDrawable::CLOSEST_SIDE: + return eemin( eemin( cx, w - cx ), eemin( cy, h - cy ) ); + case RadialGradientDrawable::FARTHEST_SIDE: + return eemax( eemax( cx, w - cx ), eemax( cy, h - cy ) ); + case RadialGradientDrawable::CLOSEST_CORNER: + return eemin( eemin( dTL, dTR ), eemin( dBL, dBR ) ); + case RadialGradientDrawable::FARTHEST_CORNER: + default: + return eemax( eemax( dTL, dTR ), eemax( dBL, dBR ) ); + } +} + +void RadialGradientDrawable::draw( const Vector2f& position, const Sizef& size ) { + if ( mColorStops.size() < 2 || size.getWidth() <= 0 || size.getHeight() <= 0 ) + return; + + Float w = size.getWidth(); + Float h = size.getHeight(); + + Float cx = mCenter.x * w; + Float cy = mCenter.y * h; + Float maxR = computeRadius( cx, cy, w, h, mExtent ); + + const int SEGMENTS = 48; + Float angleStep = 2.f * EE_PI / (Float)SEGMENTS; + + BatchRenderer* sBR = GlobalBatchRenderer::instance(); + sBR->setTexture( NULL ); + sBR->setBlendMode( BlendMode::Alpha() ); + sBR->quadsBegin(); + + Float posX = position.x; + Float posY = position.y; + + Float cosVals[SEGMENTS + 1]; + Float sinVals[SEGMENTS + 1]; + for ( int j = 0; j <= SEGMENTS; j++ ) { + Float a = (Float)j * angleStep; + cosVals[j] = std::cos( a ); + sinVals[j] = std::sin( a ); + } + + if ( isRepeating() ) { + Float firstPos = mColorStops.front().position; + Float lastPos = mColorStops.back().position; + Float patternLen = lastPos - firstPos; + if ( patternLen < 0.0001f ) { + sBR->draw(); + return; + } + + int rStart = (int)std::floor( ( -firstPos ) / patternLen ); + int rEnd = (int)std::ceil( ( 1.f - lastPos ) / patternLen ); + const int MAX_REPEATS = 64; + if ( rEnd - rStart > MAX_REPEATS ) + rEnd = rStart + MAX_REPEATS; + + for ( int r = rStart; r <= rEnd; r++ ) { + Float repeatOff = (Float)r * patternLen; + + for ( size_t i = 0; i + 1 < mColorStops.size(); i++ ) { + Float p0 = mColorStops[i].position + repeatOff; + Float p1 = mColorStops[i + 1].position + repeatOff; + + if ( p1 <= 0.f || p0 >= 1.f ) + continue; + + Float clip0 = eemax( 0.f, p0 ); + Float clip1 = eemin( 1.f, p1 ); + if ( clip1 - clip0 < 0.0001f ) + continue; + + Float r0 = clip0 * maxR; + Float r1 = clip1 * maxR; + + Float bw = p1 - p0; + Float frac0 = ( clip0 - p0 ) / bw; + Float frac1 = ( clip1 - p0 ) / bw; + const Color& sc0 = mColorStops[i].color; + const Color& sc1 = mColorStops[i + 1].color; + Color cc0( (Uint8)( (Float)sc0.r + frac0 * (Float)( sc1.r - sc0.r ) ), + (Uint8)( (Float)sc0.g + frac0 * (Float)( sc1.g - sc0.g ) ), + (Uint8)( (Float)sc0.b + frac0 * (Float)( sc1.b - sc0.b ) ), + (Uint8)( (Float)sc0.a + frac0 * (Float)( sc1.a - sc0.a ) ) ); + Color cc1( (Uint8)( (Float)sc0.r + frac1 * (Float)( sc1.r - sc0.r ) ), + (Uint8)( (Float)sc0.g + frac1 * (Float)( sc1.g - sc0.g ) ), + (Uint8)( (Float)sc0.b + frac1 * (Float)( sc1.b - sc0.b ) ), + (Uint8)( (Float)sc0.a + frac1 * (Float)( sc1.a - sc0.a ) ) ); + + Color fc0 = ( mColor.a == 255 ) ? cc0 : Color( cc0 ).blendAlpha( mColor.a ); + Color fc1 = ( mColor.a == 255 ) ? cc1 : Color( cc1 ).blendAlpha( mColor.a ); + sBR->quadsSetColorFree( fc0, fc1, fc1, fc0 ); + + for ( int j = 0; j < SEGMENTS; j++ ) { + Float cj = cosVals[j], sj = sinVals[j]; + Float cj1 = cosVals[j + 1], sj1 = sinVals[j + 1]; + + sBR->batchQuadFree( cx + r0 * cj + posX, cy + r0 * sj + posY, + cx + r1 * cj + posX, cy + r1 * sj + posY, + cx + r1 * cj1 + posX, cy + r1 * sj1 + posY, + cx + r0 * cj1 + posX, cy + r0 * sj1 + posY ); + } + } + } + } else { + for ( size_t i = 0; i + 1 < mColorStops.size(); i++ ) { + Float r0 = mColorStops[i].position * maxR; + Float r1 = mColorStops[i + 1].position * maxR; + + const Color& c0 = mColorStops[i].color; + const Color& c1 = mColorStops[i + 1].color; + Color fc0 = ( mColor.a == 255 ) ? c0 : Color( c0 ).blendAlpha( mColor.a ); + Color fc1 = ( mColor.a == 255 ) ? c1 : Color( c1 ).blendAlpha( mColor.a ); + + sBR->quadsSetColorFree( fc0, fc1, fc1, fc0 ); + + for ( int j = 0; j < SEGMENTS; j++ ) { + Float cj = cosVals[j], sj = sinVals[j]; + Float cj1 = cosVals[j + 1], sj1 = sinVals[j + 1]; + + sBR->batchQuadFree( cx + r0 * cj + posX, cy + r0 * sj + posY, cx + r1 * cj + posX, + cy + r1 * sj + posY, cx + r1 * cj1 + posX, cy + r1 * sj1 + posY, + cx + r0 * cj1 + posX, cy + r0 * sj1 + posY ); + } + } + } + + sBR->draw(); +} + +const std::vector& +RadialGradientDrawable::getColorStops() const { + return mColorStops; +} + +void RadialGradientDrawable::setColorStops( std::vector stops ) { + mColorStops = std::move( stops ); + if ( mColorStops.size() >= 2 ) { + std::sort( + mColorStops.begin(), mColorStops.end(), + []( const ColorStop& a, const ColorStop& b ) { return a.position < b.position; } ); + if ( !isRepeating() ) { + mColorStops.front().position = 0.f; + mColorStops.back().position = 1.f; + } + } +} + +RadialGradientDrawable::ShapeType RadialGradientDrawable::getShape() const { + return mShape; +} + +void RadialGradientDrawable::setShape( ShapeType shape ) { + mShape = shape; +} + +RadialGradientDrawable::Extent RadialGradientDrawable::getExtent() const { + return mExtent; +} + +void RadialGradientDrawable::setExtent( Extent extent ) { + mExtent = extent; +} + +const Vector2f& RadialGradientDrawable::getCenter() const { + return mCenter; +} + +void RadialGradientDrawable::setCenter( const Vector2f& centerNormalized ) { + mCenter = centerNormalized; +} + +void RadialGradientDrawable::setSize( const Sizef& size ) { + mSize = size; +} + +bool RadialGradientDrawable::isRepeating() const { + return mDrawableType == REPEATINGRADIALGRADIENT; +} + +}} // namespace EE::Graphics diff --git a/src/eepp/ui/css/drawableimageparser.cpp b/src/eepp/ui/css/drawableimageparser.cpp index d204d9ba4..0b6c8730a 100644 --- a/src/eepp/ui/css/drawableimageparser.cpp +++ b/src/eepp/ui/css/drawableimageparser.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include #include @@ -22,9 +23,32 @@ struct ColorStopResult { bool valid{ false }; bool isHint{ false }; Color color; - Float position{ -1.f }; /* -1 = not specified */ + Float position{ -1.f }; /* -1 = not specified */ + Float secondPosition{ -1.f }; /* -1 = single-position stop */ }; +static Float parsePositionValue( const std::string& token ) { + if ( token.empty() ) + return -1.f; + Float val; + if ( token.back() == '%' ) { + std::string num = token.substr( 0, token.size() - 1 ); + if ( String::fromString( val, num ) ) + return eemax( 0.f, eemin( 100.f, val ) ) / 100.f; + return -1.f; + } + if ( token.size() > 2 && ( token.substr( token.size() - 2 ) == "px" || + token.substr( token.size() - 2 ) == "dp" ) ) { + std::string num = token.substr( 0, token.size() - 2 ); + if ( String::fromString( val, num ) ) + return eemax( 0.f, eemin( 100.f, val ) ) / 100.f; + return -1.f; + } + if ( String::fromString( val, token ) ) + return eemax( 0.f, eemin( 100.f, val ) ) / 100.f; + return -1.f; +} + static ColorStopResult parseColorStop( const std::string& stopStr ) { ColorStopResult result; @@ -45,7 +69,7 @@ static ColorStopResult parseColorStop( const std::string& stopStr ) { } } - // Find the last whitespace that separates color from position + // Find the last whitespace that separates color from position(s) size_t lastSpace = std::string::npos; for ( size_t i = str.size(); i > 0; i-- ) { if ( str[i - 1] == ' ' ) { @@ -69,23 +93,38 @@ static ColorStopResult parseColorStop( const std::string& stopStr ) { return result; } - // Try to parse the position part String::trimInPlace( posPart ); - Float posVal = -1.f; - if ( !posPart.empty() ) { - if ( posPart.back() == '%' ) { - std::string num = posPart.substr( 0, posPart.size() - 1 ); - if ( String::fromString( posVal, num ) ) { - posVal = eemax( 0.f, eemin( 100.f, posVal ) ) / 100.f; + String::trimInPlace( colorPart ); + + // Try to split colorPart further for two-length stops (e.g. "red 0 10px") + if ( !Color::isColorString( colorPart ) ) { + size_t innerSpace = std::string::npos; + for ( size_t i = colorPart.size(); i > 0; i-- ) { + if ( colorPart[i - 1] == ' ' ) { + innerSpace = i - 1; + break; } - } else { - if ( String::fromString( posVal, posPart ) ) { - posVal = eemax( 0.f, eemin( 100.f, posVal ) ) / 100.f; + } + if ( innerSpace != std::string::npos ) { + std::string altColor = colorPart.substr( 0, innerSpace ); + String::trimInPlace( altColor ); + if ( Color::isColorString( altColor ) ) { + std::string firstPos = colorPart.substr( innerSpace + 1 ); + String::trimInPlace( firstPos ); + Float p1 = parsePositionValue( firstPos ); + Float p2 = parsePositionValue( posPart ); + result.color = Color::fromString( altColor ); + result.position = p1; + result.secondPosition = p2 >= 0.f ? p2 : -1.f; + result.valid = true; + return result; } } } - String::trimInPlace( colorPart ); + // Try to parse the position part (single position) + Float posVal = parsePositionValue( posPart ); + if ( Color::isColorString( colorPart ) ) { result.color = Color::fromString( colorPart ); result.position = posVal; @@ -141,8 +180,9 @@ void DrawableImageParser::addParser( const std::string& name, } void DrawableImageParser::registerBaseParsers() { - mFuncs["linear-gradient"] = []( const FunctionString& functionType, const Sizef& /*size*/, - bool& ownIt, UINode* node ) -> Drawable* { + // Shared parsing logic for linear-gradient and repeating-linear-gradient + auto parseGradient = []( const FunctionString& functionType, bool& ownIt, UINode* node, + bool repeating ) -> Drawable* { const auto& params( functionType.getParameters() ); if ( params.size() < 2 ) return NULL; @@ -175,7 +215,6 @@ void DrawableImageParser::registerBaseParsers() { angle = 315.f; } } else if ( !Color::isColorString( firstParam ) && !functionType.parameterWasString( 0 ) ) { - // Try to parse angle value std::string angleStr = firstParam; String::toLowerInPlace( angleStr ); @@ -196,7 +235,7 @@ void DrawableImageParser::registerBaseParsers() { } else if ( String::endsWith( s, "grad" ) ) { std::string num = s.substr( 0, s.size() - 4 ); if ( String::fromString( val, num ) ) { - out = val * 0.9f; /* 1 grad = 0.9 deg */ + out = val * 0.9f; return true; } } else if ( String::endsWith( s, "turn" ) ) { @@ -206,7 +245,7 @@ void DrawableImageParser::registerBaseParsers() { return true; } } else if ( String::fromString( val, s ) ) { - out = val; /* bare number treated as degrees */ + out = val; return true; } return false; @@ -228,6 +267,39 @@ void DrawableImageParser::registerBaseParsers() { continue; gradientStops.push_back( stop ); + if ( stop.secondPosition >= 0.f ) { + ColorStopResult stop2; + stop2.valid = true; + stop2.color = stop.color; + stop2.position = stop.secondPosition; + stop2.secondPosition = -1.f; + gradientStops.push_back( stop2 ); + } + } + + // CSS §4.3: If a color-stop has a position that is less than + // the specified position of any color-stop before it in the + // list, set its position to be equal to the largest specified + // position of any color-stop before it. This ensures hard + // transitions instead of accidental gradients. + { + Float maxPos = -1.f; + for ( auto& s : gradientStops ) { + if ( s.isHint ) + continue; + if ( s.position >= 0.f ) { + if ( s.position < maxPos ) + s.position = maxPos; + else + maxPos = s.position; + } + if ( s.secondPosition >= 0.f ) { + if ( s.secondPosition < maxPos ) + s.secondPosition = maxPos; + else + maxPos = s.secondPosition; + } + } } // Need at least two color stops (hints don't count) @@ -248,6 +320,21 @@ void DrawableImageParser::registerBaseParsers() { return a.position < b.position; } ); + // Set position 0 for the first unpositioned color stop, and position 1 + // for the last, so interior auto-distribution has proper boundaries. + for ( size_t i = 0; i < gradientStops.size(); i++ ) { + if ( !gradientStops[i].isHint && gradientStops[i].position < 0.f ) { + gradientStops[i].position = 0.f; + break; + } + } + for ( size_t i = gradientStops.size(); i > 0; i-- ) { + if ( !gradientStops[i - 1].isHint && gradientStops[i - 1].position < 0.f ) { + gradientStops[i - 1].position = 1.f; + break; + } + } + // Fill missing positions on color stops. Hints always have explicit // positions so they are left untouched. for ( size_t i = 0; i < gradientStops.size(); i++ ) { @@ -279,17 +366,21 @@ void DrawableImageParser::registerBaseParsers() { i = nextKnown; } - // Clamp first and last color stops. Skip hints at the boundaries. - for ( size_t i = 0; i < gradientStops.size(); i++ ) { - if ( !gradientStops[i].isHint ) { - gradientStops[i].position = 0.f; - break; + // Non-repeating: force first/last color stops to exactly 0/1 so + // that the gradient fills the entire box (CSS §4.3). Repeating + // mode keeps raw positions for tiling. + if ( !repeating ) { + for ( size_t i = 0; i < gradientStops.size(); i++ ) { + if ( !gradientStops[i].isHint ) { + gradientStops[i].position = 0.f; + break; + } } - } - for ( size_t i = gradientStops.size(); i > 0; i-- ) { - if ( !gradientStops[i - 1].isHint ) { - gradientStops[i - 1].position = 1.f; - break; + for ( size_t i = gradientStops.size(); i > 0; i-- ) { + if ( !gradientStops[i - 1].isHint ) { + gradientStops[i - 1].position = 1.f; + break; + } } } @@ -356,13 +447,254 @@ void DrawableImageParser::registerBaseParsers() { if ( stops.size() < 2 ) return NULL; - LinearGradientDrawable* drawable = LinearGradientDrawable::New(); + LinearGradientDrawable* drawable = + repeating ? LinearGradientDrawable::NewRepeating() : LinearGradientDrawable::New(); drawable->setColorStops( std::move( stops ) ); drawable->setAngle( angle ); ownIt = true; return drawable; }; + mFuncs["linear-gradient"] = [parseGradient]( const FunctionString& functionType, + const Sizef& /*size*/, bool& ownIt, + UINode* node ) -> Drawable* { + return parseGradient( functionType, ownIt, node, false ); + }; + + mFuncs["repeating-linear-gradient"] = [parseGradient]( const FunctionString& functionType, + const Sizef& /*size*/, bool& ownIt, + UINode* node ) -> Drawable* { + return parseGradient( functionType, ownIt, node, true ); + }; + + // Shared parsing logic for radial-gradient and repeating-radial-gradient + auto parseRadialGradient = []( const FunctionString& functionType, bool& ownIt, UINode* node, + bool repeating ) -> Drawable* { + const auto& params( functionType.getParameters() ); + if ( params.size() < 2 ) + return NULL; + + size_t paramIdx = 0; + RadialGradientDrawable::ShapeType shape = RadialGradientDrawable::CIRCLE; + RadialGradientDrawable::Extent extent = RadialGradientDrawable::FARTHEST_CORNER; + Vector2f center( 0.5f, 0.5f ); + + if ( paramIdx < params.size() ) { + std::string kw = String::toLower( String::trim( params[paramIdx] ) ); + if ( kw == "circle" ) { + shape = RadialGradientDrawable::CIRCLE; + paramIdx++; + } else if ( kw == "ellipse" ) { + shape = RadialGradientDrawable::ELLIPSE; + paramIdx++; + } + } + + std::vector gradientStops; + for ( size_t i = paramIdx; i < params.size(); i++ ) { + std::string stopParam = params[i]; + String::trimInPlace( stopParam ); + + ColorStopResult stop = parseColorStop( stopParam ); + if ( !stop.valid ) + continue; + + gradientStops.push_back( stop ); + if ( stop.secondPosition >= 0.f ) { + ColorStopResult stop2; + stop2.valid = true; + stop2.color = stop.color; + stop2.position = stop.secondPosition; + stop2.secondPosition = -1.f; + gradientStops.push_back( stop2 ); + } + } + + // CSS §4.3: If a color-stop has a position that is less than + // the specified position of any color-stop before it in the + // list, set its position to be equal to the largest specified + // position of any color-stop before it. + { + Float maxPos = -1.f; + for ( auto& s : gradientStops ) { + if ( s.isHint ) + continue; + if ( s.position >= 0.f ) { + if ( s.position < maxPos ) + s.position = maxPos; + else + maxPos = s.position; + } + if ( s.secondPosition >= 0.f ) { + if ( s.secondPosition < maxPos ) + s.secondPosition = maxPos; + else + maxPos = s.secondPosition; + } + } + } + + { + int colorStopCount = 0; + for ( const auto& s : gradientStops ) { + if ( !s.isHint ) + colorStopCount++; + } + if ( colorStopCount < 2 ) + return NULL; + } + + std::sort( gradientStops.begin(), gradientStops.end(), + []( const ColorStopResult& a, const ColorStopResult& b ) { + return a.position < b.position; + } ); + + // Set boundary defaults for unpositioned stops + for ( size_t i = 0; i < gradientStops.size(); i++ ) { + if ( !gradientStops[i].isHint && gradientStops[i].position < 0.f ) { + gradientStops[i].position = 0.f; + break; + } + } + for ( size_t i = gradientStops.size(); i > 0; i-- ) { + if ( !gradientStops[i - 1].isHint && gradientStops[i - 1].position < 0.f ) { + gradientStops[i - 1].position = 1.f; + break; + } + } + + // Fill missing positions on color stops. Hints always have explicit + // positions so they are left untouched. + for ( size_t i = 0; i < gradientStops.size(); i++ ) { + if ( gradientStops[i].isHint || gradientStops[i].position >= 0.f ) + continue; + size_t prevKnown = i; + size_t nextKnown = i; + while ( prevKnown > 0 && ( gradientStops[prevKnown - 1].position < 0.f ) ) + prevKnown--; + while ( nextKnown < gradientStops.size() && + ( gradientStops[nextKnown].isHint || gradientStops[nextKnown].position < 0.f ) ) + nextKnown++; + + Float startPos = ( prevKnown > 0 && !gradientStops[prevKnown - 1].isHint ) + ? gradientStops[prevKnown - 1].position + : 0.f; + Float endPos = + ( nextKnown < gradientStops.size() ) ? gradientStops[nextKnown].position : 1.f; + size_t range = nextKnown - prevKnown + 1; + + for ( size_t j = prevKnown; j < nextKnown; j++ ) { + if ( gradientStops[j].isHint ) + continue; + gradientStops[j].position = + startPos + + ( endPos - startPos ) * ( (Float)( j - prevKnown + 1 ) / (Float)range ); + } + + i = nextKnown; + } + + // Non-repeating: force first/last color stops to exactly 0/1 + if ( !repeating ) { + for ( size_t i = 0; i < gradientStops.size(); i++ ) { + if ( !gradientStops[i].isHint ) { + gradientStops[i].position = 0.f; + break; + } + } + for ( size_t i = gradientStops.size(); i > 0; i-- ) { + if ( !gradientStops[i - 1].isHint ) { + gradientStops[i - 1].position = 1.f; + break; + } + } + } + + // Expand hinted segments into finely sampled color stops + std::vector stops; + const int HINT_SAMPLES = 16; + + for ( size_t i = 0; i < gradientStops.size(); i++ ) { + if ( gradientStops[i].isHint ) + continue; + + // Add the current color stop + stops.push_back( RadialGradientDrawable::ColorStop( gradientStops[i].position, + gradientStops[i].color ) ); + + // Find the next color stop and any hints between them + size_t nextColor = i + 1; + while ( nextColor < gradientStops.size() && gradientStops[nextColor].isHint ) + nextColor++; + if ( nextColor >= gradientStops.size() ) + break; + + // Collect hints between current and next color stop + std::vector hints; + for ( size_t h = i + 1; h < nextColor; h++ ) { + if ( gradientStops[h].isHint ) + hints.push_back( gradientStops[h].position ); + } + + if ( hints.empty() ) + continue; + + Float p0 = gradientStops[i].position; + Float p1 = gradientStops[nextColor].position; + Color c0 = gradientStops[i].color; + Color c1 = gradientStops[nextColor].color; + + if ( std::abs( p1 - p0 ) < 0.0001f ) + continue; + + // Apply hints sequentially. Each hint adjusts the transition + // between the previously computed mid-points. + // For a single hint at position h between p0 and p1: + // normalized hint hn = (h - p0) / (p1 - p0) + // power = log(0.5) / log(hn) + // color(p) = lerp(c0, c1, ((p-p0)/(p1-p0))^power) + // We sample HINT_SAMPLES points and add them as color stops. + Float hintNorm = ( hints[0] - p0 ) / ( p1 - p0 ); + hintNorm = eemax( 0.001f, eemin( 0.999f, hintNorm ) ); + Float power = std::log( 0.5f ) / std::log( hintNorm ); + + for ( int s = 1; s < HINT_SAMPLES; s++ ) { + Float x = (Float)s / (Float)HINT_SAMPLES; + Float t = std::pow( x, power ); + Float pos = p0 + x * ( p1 - p0 ); + Uint8 r = (Uint8)( (Float)c0.r + t * (Float)( c1.r - c0.r ) ); + Uint8 g = (Uint8)( (Float)c0.g + t * (Float)( c1.g - c0.g ) ); + Uint8 b = (Uint8)( (Float)c0.b + t * (Float)( c1.b - c0.b ) ); + Uint8 a = (Uint8)( (Float)c0.a + t * (Float)( c1.a - c0.a ) ); + stops.push_back( RadialGradientDrawable::ColorStop( pos, Color( r, g, b, a ) ) ); + } + } + + if ( stops.size() < 2 ) + return NULL; + + RadialGradientDrawable* drawable = + repeating ? RadialGradientDrawable::NewRepeating() : RadialGradientDrawable::New(); + drawable->setColorStops( std::move( stops ) ); + drawable->setShape( shape ); + drawable->setExtent( extent ); + drawable->setCenter( center ); + ownIt = true; + return drawable; + }; + + mFuncs["radial-gradient"] = [parseRadialGradient]( const FunctionString& functionType, + const Sizef& /*size*/, bool& ownIt, + UINode* node ) -> Drawable* { + return parseRadialGradient( functionType, ownIt, node, false ); + }; + + mFuncs["repeating-radial-gradient"] = [parseRadialGradient]( const FunctionString& functionType, + const Sizef& /*size*/, bool& ownIt, + UINode* node ) -> Drawable* { + return parseRadialGradient( functionType, ownIt, node, true ); + }; + mFuncs["circle"] = []( const FunctionString& functionType, const Sizef& size, bool& ownIt, UINode* node ) -> Drawable* { if ( functionType.getParameters().size() < 1 ) { diff --git a/src/eepp/ui/uinodedrawable.cpp b/src/eepp/ui/uinodedrawable.cpp index 68981fe5d..66ceba561 100644 --- a/src/eepp/ui/uinodedrawable.cpp +++ b/src/eepp/ui/uinodedrawable.cpp @@ -711,7 +711,10 @@ Sizef UINodeDrawable::LayerDrawable::calcDrawableSize( const std::string& drawab if ( drawableSizeEq == "auto" ) { if ( mDrawable->getDrawableType() == Drawable::RECTANGLE || - mDrawable->getDrawableType() == LINEARGRADIENT ) { + mDrawable->getDrawableType() == LINEARGRADIENT || + mDrawable->getDrawableType() == REPEATINGLINEARGRADIENT || + mDrawable->getDrawableType() == RADIALGRADIENT || + mDrawable->getDrawableType() == REPEATINGRADIALGRADIENT ) { size = mSize; } else { size = mDrawable->getPixelsSize(); @@ -749,7 +752,10 @@ Sizef UINodeDrawable::LayerDrawable::calcDrawableSize( const std::string& drawab if ( sizePart.size() == 2 ) { if ( sizePart[0] == "auto" && sizePart[1] == "auto" ) { if ( mDrawable->getDrawableType() == Drawable::RECTANGLE || - mDrawable->getDrawableType() == Drawable::LINEARGRADIENT ) { + mDrawable->getDrawableType() == Drawable::LINEARGRADIENT || + mDrawable->getDrawableType() == Drawable::REPEATINGLINEARGRADIENT || + mDrawable->getDrawableType() == Drawable::RADIALGRADIENT || + mDrawable->getDrawableType() == Drawable::REPEATINGRADIALGRADIENT ) { size = mSize; } else { size = mDrawable->getSize(); diff --git a/src/tests/unit_tests/drawableimageparser_tests.cpp b/src/tests/unit_tests/drawableimageparser_tests.cpp index 2507628b8..0ded111b7 100644 --- a/src/tests/unit_tests/drawableimageparser_tests.cpp +++ b/src/tests/unit_tests/drawableimageparser_tests.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include #include @@ -260,3 +261,203 @@ UTEST( DrawableImageParser, EmptyAngleFallsThrough ) { eeSAFE_DELETE( drawable ); Engine::destroySingleton(); } + +UTEST( DrawableImageParser, RepeatingTwoStops ) { + Engine::instance()->createWindow( WindowSettings( 512, 512, "DrawableImageParser test", + WindowStyle::Default, WindowBackend::Default, + 32, {}, 1, false, true ), + ContextSettings( false, 0, 0, GLv_default, true, false ) ); + UISceneNode* sceneNode = initDrawableParserTest(); + UINode* node = createTestNode( sceneNode ); + + auto& parser = StyleSheetSpecification::instance()->getDrawableImageParser(); + bool ownIt = false; + Drawable* drawable = parser.createDrawable( "repeating-linear-gradient(red, blue)", + Sizef( 100, 100 ), ownIt, node ); + + ASSERT_TRUE( drawable != NULL ); + ASSERT_TRUE( drawable->getDrawableType() == Drawable::REPEATINGLINEARGRADIENT ); + auto* grad = static_cast( drawable ); + EXPECT_TRUE( grad->isRepeating() ); + const auto& stops = grad->getColorStops(); + ASSERT_EQ( stops.size(), (size_t)2 ); + EXPECT_EQ( stops[0].position, 0.f ); + EXPECT_EQ( stops[1].position, 1.f ); + + eeSAFE_DELETE( drawable ); + Engine::destroySingleton(); +} + +UTEST( DrawableImageParser, RepeatingWithAngle ) { + Engine::instance()->createWindow( WindowSettings( 512, 512, "DrawableImageParser test", + WindowStyle::Default, WindowBackend::Default, + 32, {}, 1, false, true ), + ContextSettings( false, 0, 0, GLv_default, true, false ) ); + UISceneNode* sceneNode = initDrawableParserTest(); + UINode* node = createTestNode( sceneNode ); + + auto& parser = StyleSheetSpecification::instance()->getDrawableImageParser(); + bool ownIt = false; + Drawable* drawable = parser.createDrawable( + "repeating-linear-gradient(45deg, #f00, #0f0, #00f)", Sizef( 100, 100 ), ownIt, node ); + + ASSERT_TRUE( drawable != NULL ); + auto* grad = static_cast( drawable ); + EXPECT_EQ( grad->getAngle(), 45.f ); + EXPECT_TRUE( grad->isRepeating() ); + const auto& stops = grad->getColorStops(); + ASSERT_EQ( stops.size(), (size_t)3 ); + + eeSAFE_DELETE( drawable ); + Engine::destroySingleton(); +} + +UTEST( DrawableImageParser, RepeatingWithPositions ) { + Engine::instance()->createWindow( WindowSettings( 512, 512, "DrawableImageParser test", + WindowStyle::Default, WindowBackend::Default, + 32, {}, 1, false, true ), + ContextSettings( false, 0, 0, GLv_default, true, false ) ); + UISceneNode* sceneNode = initDrawableParserTest(); + UINode* node = createTestNode( sceneNode ); + + auto& parser = StyleSheetSpecification::instance()->getDrawableImageParser(); + bool ownIt = false; + Drawable* drawable = parser.createDrawable( "repeating-linear-gradient(red 10%, blue 40%)", + Sizef( 100, 100 ), ownIt, node ); + + ASSERT_TRUE( drawable != NULL ); + auto* grad = static_cast( drawable ); + EXPECT_TRUE( grad->isRepeating() ); + const auto& stops = grad->getColorStops(); + ASSERT_EQ( stops.size(), (size_t)2 ); + EXPECT_EQ( stops[0].position, 0.1f ); + EXPECT_EQ( stops[1].position, 0.4f ); + + eeSAFE_DELETE( drawable ); + Engine::destroySingleton(); +} + +UTEST( DrawableImageParser, RadialTwoStops ) { + Engine::instance()->createWindow( WindowSettings( 512, 512, "DrawableImageParser test", + WindowStyle::Default, WindowBackend::Default, + 32, {}, 1, false, true ), + ContextSettings( false, 0, 0, GLv_default, true, false ) ); + UISceneNode* sceneNode = initDrawableParserTest(); + UINode* node = createTestNode( sceneNode ); + + auto& parser = StyleSheetSpecification::instance()->getDrawableImageParser(); + bool ownIt = false; + Drawable* drawable = + parser.createDrawable( "radial-gradient(red, blue)", Sizef( 100, 100 ), ownIt, node ); + + ASSERT_TRUE( drawable != NULL ); + ASSERT_TRUE( drawable->getDrawableType() == Drawable::RADIALGRADIENT ); + auto* grad = static_cast( drawable ); + EXPECT_FALSE( grad->isRepeating() ); + const auto& stops = grad->getColorStops(); + ASSERT_EQ( stops.size(), (size_t)2 ); + EXPECT_EQ( stops[0].position, 0.f ); + EXPECT_EQ( stops[1].position, 1.f ); + + eeSAFE_DELETE( drawable ); + Engine::destroySingleton(); +} + +UTEST( DrawableImageParser, RadialCircleKeyword ) { + Engine::instance()->createWindow( WindowSettings( 512, 512, "DrawableImageParser test", + WindowStyle::Default, WindowBackend::Default, + 32, {}, 1, false, true ), + ContextSettings( false, 0, 0, GLv_default, true, false ) ); + UISceneNode* sceneNode = initDrawableParserTest(); + UINode* node = createTestNode( sceneNode ); + + auto& parser = StyleSheetSpecification::instance()->getDrawableImageParser(); + bool ownIt = false; + Drawable* drawable = parser.createDrawable( "radial-gradient(circle, #f00, #00f)", + Sizef( 100, 100 ), ownIt, node ); + + ASSERT_TRUE( drawable != NULL ); + auto* grad = static_cast( drawable ); + EXPECT_EQ( grad->getShape(), RadialGradientDrawable::CIRCLE ); + EXPECT_EQ( grad->getColorStops().size(), (size_t)2 ); + + eeSAFE_DELETE( drawable ); + Engine::destroySingleton(); +} + +UTEST( DrawableImageParser, RadialWithPositions ) { + Engine::instance()->createWindow( WindowSettings( 512, 512, "DrawableImageParser test", + WindowStyle::Default, WindowBackend::Default, + 32, {}, 1, false, true ), + ContextSettings( false, 0, 0, GLv_default, true, false ) ); + UISceneNode* sceneNode = initDrawableParserTest(); + UINode* node = createTestNode( sceneNode ); + + auto& parser = StyleSheetSpecification::instance()->getDrawableImageParser(); + bool ownIt = false; + Drawable* drawable = parser.createDrawable( "radial-gradient(red 10%, blue 80%)", + Sizef( 100, 100 ), ownIt, node ); + + ASSERT_TRUE( drawable != NULL ); + auto* grad = static_cast( drawable ); + const auto& stops = grad->getColorStops(); + ASSERT_EQ( stops.size(), (size_t)2 ); + EXPECT_EQ( stops[0].position, 0.f ); + EXPECT_EQ( stops[1].position, 1.f ); + + eeSAFE_DELETE( drawable ); + Engine::destroySingleton(); +} + +UTEST( DrawableImageParser, RepeatingRadial ) { + Engine::instance()->createWindow( WindowSettings( 512, 512, "DrawableImageParser test", + WindowStyle::Default, WindowBackend::Default, + 32, {}, 1, false, true ), + ContextSettings( false, 0, 0, GLv_default, true, false ) ); + UISceneNode* sceneNode = initDrawableParserTest(); + UINode* node = createTestNode( sceneNode ); + + auto& parser = StyleSheetSpecification::instance()->getDrawableImageParser(); + bool ownIt = false; + Drawable* drawable = parser.createDrawable( "repeating-radial-gradient(red 10%, blue 40%)", + Sizef( 100, 100 ), ownIt, node ); + + ASSERT_TRUE( drawable != NULL ); + ASSERT_TRUE( drawable->getDrawableType() == Drawable::REPEATINGRADIALGRADIENT ); + auto* grad = static_cast( drawable ); + EXPECT_TRUE( grad->isRepeating() ); + const auto& stops = grad->getColorStops(); + ASSERT_EQ( stops.size(), (size_t)2 ); + EXPECT_EQ( stops[0].position, 0.1f ); + EXPECT_EQ( stops[1].position, 0.4f ); + + eeSAFE_DELETE( drawable ); + Engine::destroySingleton(); +} + +UTEST( DrawableImageParser, TwoLengthStopSyntax ) { + Engine::instance()->createWindow( WindowSettings( 512, 512, "DrawableImageParser test", + WindowStyle::Default, WindowBackend::Default, + 32, {}, 1, false, true ), + ContextSettings( false, 0, 0, GLv_default, true, false ) ); + UISceneNode* sceneNode = initDrawableParserTest(); + UINode* node = createTestNode( sceneNode ); + + auto& parser = StyleSheetSpecification::instance()->getDrawableImageParser(); + bool ownIt = false; + Drawable* drawable = parser.createDrawable( + "repeating-linear-gradient(red 0 10px, blue 10px 20px)", Sizef( 100, 100 ), ownIt, node ); + + ASSERT_TRUE( drawable != NULL ); + auto* grad = static_cast( drawable ); + EXPECT_TRUE( grad->isRepeating() ); + const auto& stops = grad->getColorStops(); + ASSERT_EQ( stops.size(), (size_t)4 ); + EXPECT_EQ( stops[0].position, 0.f ); + EXPECT_EQ( stops[1].position, 0.1f ); + EXPECT_EQ( stops[2].position, 0.1f ); + EXPECT_EQ( stops[3].position, 0.2f ); + + eeSAFE_DELETE( drawable ); + Engine::destroySingleton(); +}