diff --git a/bin/unit_tests/assets/html/eepp-ui-border-rendering-2.webp b/bin/unit_tests/assets/html/eepp-ui-border-rendering-2.webp index 8f3ae1caf..a0ea9a315 100644 Binary files a/bin/unit_tests/assets/html/eepp-ui-border-rendering-2.webp and b/bin/unit_tests/assets/html/eepp-ui-border-rendering-2.webp differ diff --git a/bin/unit_tests/assets/html/eepp-uihtmltable-complex-layout-2.webp b/bin/unit_tests/assets/html/eepp-uihtmltable-complex-layout-2.webp index f06d8b567..56fdbadce 100644 Binary files a/bin/unit_tests/assets/html/eepp-uihtmltable-complex-layout-2.webp and b/bin/unit_tests/assets/html/eepp-uihtmltable-complex-layout-2.webp differ diff --git a/include/eepp/ui/border.hpp b/include/eepp/ui/border.hpp index 7d378c0be..f8bfd1a40 100644 --- a/include/eepp/ui/border.hpp +++ b/include/eepp/ui/border.hpp @@ -43,6 +43,10 @@ struct EE_API Borders { static Sizef radiusFromString( const UINode* node, const std::string& val ); + /** Resolves overlapping corner curves with the single proportional scale factor required by + * CSS Backgrounds and Borders. The input radii are not modified. */ + static BorderRadiuses normalizeRadiuses( const BorderRadiuses& radius, const Sizef& size ); + /** Creates the border geometry into the VertexBuffer provided. The VertexBuffer must be a * a EE::Graphics::PrimitiveType::PRIMITIVE_TRIANGLE_STRIP with VERTEX_FLAGS_PRIMITIVE flags. */ static void createBorders( VertexBuffer* vbo, const Borders& borders, const Vector2f& pos, diff --git a/include/eepp/ui/uihelper.hpp b/include/eepp/ui/uihelper.hpp index 9566aca49..8f3d6d8a9 100644 --- a/include/eepp/ui/uihelper.hpp +++ b/include/eepp/ui/uihelper.hpp @@ -44,8 +44,9 @@ enum UIFlag : Int64 { UI_HIGHLIGHT = ( 1 << 28 ), UI_PARENT_ATTRIBUTE_CHANGED = ( 1 << 29 ), UI_LOADS_ITS_CHILDREN = ( 1 << 30 ), - UI_HTML_ELEMENT = ( 1 << 31 ), + UI_HTML_ELEMENT = ( 1ULL << 31 ), UI_CREATING_NODE = ( 1ULL << 32 ), + UI_IGNORE_GLOBAL_CSS = ( 1ULL << 33 ), }; enum UINodeType { diff --git a/include/eepp/ui/uihtmlinput.hpp b/include/eepp/ui/uihtmlinput.hpp index b67f936b3..1dd9f5736 100644 --- a/include/eepp/ui/uihtmlinput.hpp +++ b/include/eepp/ui/uihtmlinput.hpp @@ -27,6 +27,8 @@ class EE_API UIHTMLInput : public UIHTMLWidget { virtual Float getMaxIntrinsicWidth() const; + virtual void updateLayout(); + const std::string& getInputType() const; void setInputType( const std::string& type ); @@ -38,14 +40,26 @@ class EE_API UIHTMLInput : public UIHTMLWidget { protected: std::string mInputType{ "text" }; UIWidget* mChildWidget{ nullptr }; - std::map mProperties; + std::map mImplementationProperties; String mValue; bool mChecked{ false }; + bool mSyncingGeometry{ false }; + bool mHiddenByType{ false }; + bool mVisibleBeforeHidden{ true }; + bool mEnabledBeforeHidden{ true }; + CSSDisplay mDisplayBeforeHidden{ CSSDisplay::InlineBlock }; void createChildWidget(); + void configureChildWidget(); + void applyImplementationProperty( const StyleSheetProperty& property ); + void syncImplementationState(); + void syncStateFromImplementation(); + void updateHostGeometry(); + void updateChildGeometry(); void syncCheckedState(); virtual void onSizeChange(); + virtual void onPaddingChange(); }; }} // namespace EE::UI diff --git a/src/eepp/ui/border.cpp b/src/eepp/ui/border.cpp index 60a4e58f6..65c47ba2b 100644 --- a/src/eepp/ui/border.cpp +++ b/src/eepp/ui/border.cpp @@ -6,12 +6,15 @@ namespace EE { namespace UI { static void borderAddArc( VertexBuffer* vbo, Vector2f pos, Float radiW, Float radiH, double arcStartAngle, double arcEndAngle, Color color, Float lineW, - Vector2f basePos, bool decrease = false, bool addInnerVertex = true ) { + Float lineH, Vector2f basePos, bool decrease = false, + bool addInnerVertex = true ) { // TODO: Add segment count parameter to change the arc precision (num of vertex). Float angleShift = 1; double startAngle = eemin( arcStartAngle, arcEndAngle ); double endAngle = eemax( arcStartAngle, arcEndAngle ); - Vector2f startPos = ( radiW > lineW ) ? pos : basePos; + const Float innerW = eemax( 0.f, radiW - lineW ); + const Float innerH = eemax( 0.f, radiH - lineH ); + Vector2f startPos = innerW > 0.f || innerH > 0.f ? pos : basePos; double lastAngle = startAngle; double i; @@ -22,9 +25,9 @@ static void borderAddArc( VertexBuffer* vbo, Vector2f pos, Float radiW, Float ra vbo->addColor( color ); if ( addInnerVertex ) { - if ( radiW > lineW ) { - vbo->addVertex( Vector2f( pos.x + ( radiW - lineW ) * Math::cosAng( i ), - pos.y + ( radiH - lineW ) * Math::sinAng( i ) ) ); + if ( innerW > 0.f || innerH > 0.f ) { + vbo->addVertex( Vector2f( pos.x + innerW * Math::cosAng( i ), + pos.y + innerH * Math::sinAng( i ) ) ); } else { vbo->addVertex( startPos ); } @@ -42,9 +45,9 @@ static void borderAddArc( VertexBuffer* vbo, Vector2f pos, Float radiW, Float ra vbo->addColor( color ); if ( addInnerVertex ) { - if ( radiW > lineW ) { - vbo->addVertex( Vector2f( pos.x + ( radiW - lineW ) * Math::cosAng( i ), - pos.y + ( radiH - lineW ) * Math::sinAng( i ) ) ); + if ( innerW > 0.f || innerH > 0.f ) { + vbo->addVertex( Vector2f( pos.x + innerW * Math::cosAng( i ), + pos.y + innerH * Math::sinAng( i ) ) ); } else { vbo->addVertex( startPos ); } @@ -54,9 +57,9 @@ static void borderAddArc( VertexBuffer* vbo, Vector2f pos, Float radiW, Float ra } else { for ( i = startAngle; i <= endAngle; i += angleShift ) { if ( addInnerVertex ) { - if ( radiW > lineW ) { - vbo->addVertex( Vector2f( pos.x + ( radiW - lineW ) * Math::cosAng( i ), - pos.y + ( radiH - lineW ) * Math::sinAng( i ) ) ); + if ( innerW > 0.f || innerH > 0.f ) { + vbo->addVertex( Vector2f( pos.x + innerW * Math::cosAng( i ), + pos.y + innerH * Math::sinAng( i ) ) ); } else { vbo->addVertex( startPos ); } @@ -75,9 +78,9 @@ static void borderAddArc( VertexBuffer* vbo, Vector2f pos, Float radiW, Float ra i = endAngle; if ( addInnerVertex ) { - if ( radiW > lineW ) { - vbo->addVertex( Vector2f( pos.x + ( radiW - lineW ) * Math::cosAng( i ), - pos.y + ( radiH - lineW ) * Math::sinAng( i ) ) ); + if ( innerW > 0.f || innerH > 0.f ) { + vbo->addVertex( Vector2f( pos.x + innerW * Math::cosAng( i ), + pos.y + innerH * Math::sinAng( i ) ) ); } else { vbo->addVertex( startPos ); } @@ -122,6 +125,38 @@ Sizef Borders::radiusFromString( const UINode* node, const std::string& val ) { return size; } +BorderRadiuses Borders::normalizeRadiuses( const BorderRadiuses& radius, const Sizef& size ) { + BorderRadiuses usedRadius = radius; + usedRadius.topLeft.x = eemax( 0.f, usedRadius.topLeft.x ); + usedRadius.topLeft.y = eemax( 0.f, usedRadius.topLeft.y ); + usedRadius.topRight.x = eemax( 0.f, usedRadius.topRight.x ); + usedRadius.topRight.y = eemax( 0.f, usedRadius.topRight.y ); + usedRadius.bottomRight.x = eemax( 0.f, usedRadius.bottomRight.x ); + usedRadius.bottomRight.y = eemax( 0.f, usedRadius.bottomRight.y ); + usedRadius.bottomLeft.x = eemax( 0.f, usedRadius.bottomLeft.x ); + usedRadius.bottomLeft.y = eemax( 0.f, usedRadius.bottomLeft.y ); + + Float factor = 1.f; + auto constrain = [&factor]( Float available, Float sum ) { + if ( sum > 0.f ) + factor = eemin( factor, available / sum ); + }; + const Float width = eemax( 0.f, size.getWidth() ); + const Float height = eemax( 0.f, size.getHeight() ); + constrain( width, usedRadius.topLeft.x + usedRadius.topRight.x ); + constrain( width, usedRadius.bottomLeft.x + usedRadius.bottomRight.x ); + constrain( height, usedRadius.topLeft.y + usedRadius.bottomLeft.y ); + constrain( height, usedRadius.topRight.y + usedRadius.bottomRight.y ); + + if ( factor < 1.f ) { + usedRadius.topLeft *= factor; + usedRadius.topRight *= factor; + usedRadius.bottomRight *= factor; + usedRadius.bottomLeft *= factor; + } + return usedRadius; +} + void Borders::createBorders( VertexBuffer* vbo, const Borders& borders, const Vector2f& pos, const Sizef& size ) { vbo->clear(); @@ -130,8 +165,6 @@ void Borders::createBorders( VertexBuffer* vbo, const Borders& borders, const Ve int borderBottom = 0; int borderLeft = 0; int borderRight = 0; - Float halfWidth = size.getWidth() * 0.5f; - Float halfHeight = size.getHeight() * 0.5f; if ( borders.top.width >= 0 ) { borderTop = eemin( (int)( size.getHeight() * 0.5f ), (int)borders.top.width ); @@ -157,15 +190,16 @@ void Borders::createBorders( VertexBuffer* vbo, const Borders& borders, const Ve if ( !hasTop && !hasRight && !hasBottom && !hasLeft ) return; - // Pre-compute arc radii for each corner - double tlArcW = eemin( halfWidth, eemax( 0.f, borders.radius.topLeft.x ) ); - double tlArcH = eemin( halfWidth, eemax( 0.f, borders.radius.topLeft.y ) ); - double trArcW = eemin( halfHeight, eemax( 0.f, borders.radius.topRight.x ) ); - double trArcH = eemin( halfHeight, eemax( 0.f, borders.radius.topRight.y ) ); - double brArcW = eemin( halfHeight, eemax( 0.f, borders.radius.bottomRight.x ) ); - double brArcH = eemin( halfHeight, eemax( 0.f, borders.radius.bottomRight.y ) ); - double blArcW = eemin( halfWidth, eemax( 0.f, borders.radius.bottomLeft.x ) ); - double blArcH = eemin( halfWidth, eemax( 0.f, borders.radius.bottomLeft.y ) ); + // CSS scales every radius component by one factor when any pair of curves overlaps. + const BorderRadiuses usedRadius = normalizeRadiuses( borders.radius, size ); + double tlArcW = usedRadius.topLeft.x; + double tlArcH = usedRadius.topLeft.y; + double trArcW = usedRadius.topRight.x; + double trArcH = usedRadius.topRight.y; + double brArcW = usedRadius.bottomRight.x; + double brArcH = usedRadius.bottomRight.y; + double blArcW = usedRadius.bottomLeft.x; + double blArcH = usedRadius.bottomLeft.y; // Corner positions Vector2f tlInner( pos.x + borderLeft, pos.y + borderTop ); @@ -187,10 +221,12 @@ void Borders::createBorders( VertexBuffer* vbo, const Borders& borders, const Ve // Helper: compute arc inner vertex at a given angle auto arcInnerPos = []( const Vector2f& center, double rW, double rH, double angleDeg, - double lineW, const Vector2f& basePos ) -> Vector2f { - if ( rW > lineW ) - return Vector2f( center.x + ( rW - lineW ) * Math::cosAng( angleDeg ), - center.y + ( rH - lineW ) * Math::sinAng( angleDeg ) ); + double lineW, double lineH, const Vector2f& basePos ) -> Vector2f { + const double innerW = eemax( 0., rW - lineW ); + const double innerH = eemax( 0., rH - lineH ); + if ( innerW > 0. || innerH > 0. ) + return Vector2f( center.x + innerW * Math::cosAng( angleDeg ), + center.y + innerH * Math::sinAng( angleDeg ) ); return basePos; }; @@ -199,9 +235,8 @@ void Borders::createBorders( VertexBuffer* vbo, const Borders& borders, const Ve Vector2f topFirstInner; if ( tlArcW > 0 && hasLeft ) { Vector2f tlCenter( pos.x + tlArcW, pos.y + tlArcH ); - topFirstInner = - arcInnerPos( tlCenter, tlArcW, tlArcH, 225, borderTop, - Vector2f( pos.x + borderLeft, pos.y + borderTop ) ); + topFirstInner = arcInnerPos( tlCenter, tlArcW, tlArcH, 225, borderLeft, borderTop, + Vector2f( pos.x + borderLeft, pos.y + borderTop ) ); } else { topFirstInner = tlInner; } @@ -211,7 +246,7 @@ void Borders::createBorders( VertexBuffer* vbo, const Borders& borders, const Ve if ( trArcW > 0 && hasTop ) { Vector2f trCenter( pos.x + size.getWidth() - trArcW, pos.y + trArcH ); rightFirstInner = - arcInnerPos( trCenter, trArcW, trArcH, 315, borderRight, + arcInnerPos( trCenter, trArcW, trArcH, 315, borderRight, borderTop, Vector2f( pos.x + size.getWidth() - borderRight, pos.y + borderTop ) ); } else { rightFirstInner = trInner; @@ -220,12 +255,10 @@ void Borders::createBorders( VertexBuffer* vbo, const Borders& borders, const Ve // Bottom border first inner (bottom-right corner) Vector2f bottomFirstInner; if ( brArcW > 0 && hasRight ) { - Vector2f brCenter( pos.x + size.getWidth() - brArcW, - pos.y + size.getHeight() - brArcH ); - bottomFirstInner = - arcInnerPos( brCenter, brArcW, brArcH, 45, borderBottom, - Vector2f( pos.x + size.getWidth() - borderRight, - pos.y + size.getHeight() - borderBottom ) ); + Vector2f brCenter( pos.x + size.getWidth() - brArcW, pos.y + size.getHeight() - brArcH ); + bottomFirstInner = arcInnerPos( brCenter, brArcW, brArcH, 45, borderRight, borderBottom, + Vector2f( pos.x + size.getWidth() - borderRight, + pos.y + size.getHeight() - borderBottom ) ); } else { bottomFirstInner = brInner; } @@ -235,9 +268,8 @@ void Borders::createBorders( VertexBuffer* vbo, const Borders& borders, const Ve if ( blArcW > 0 && hasBottom ) { Vector2f blCenter( pos.x + blArcW, pos.y + size.getHeight() - blArcH ); leftFirstInner = - arcInnerPos( blCenter, blArcW, blArcH, 135, borderLeft, - Vector2f( pos.x + borderLeft, - pos.y + size.getHeight() - borderBottom ) ); + arcInnerPos( blCenter, blArcW, blArcH, 135, borderLeft, borderBottom, + Vector2f( pos.x + borderLeft, pos.y + size.getHeight() - borderBottom ) ); } else { leftFirstInner = blInner; } @@ -267,7 +299,7 @@ void Borders::createBorders( VertexBuffer* vbo, const Borders& borders, const Ve double startAngle = 225; borderAddArc( vbo, Vector2f( pos.x + leftW, pos.y + leftH ), leftW, leftH, startAngle, - endAngle, borders.top.color, borderTop, + endAngle, borders.top.color, borderLeft, borderTop, Vector2f( pos.x + borderLeft, pos.y + borderTop ) ); } else { vbo->addVertex( Vector2f( pos.x + borderLeft, pos.y + borderTop ) ); @@ -282,10 +314,10 @@ void Borders::createBorders( VertexBuffer* vbo, const Borders& borders, const Ve Vector2f basePos( pos.x + size.getWidth() - borderRight, pos.y + borderTop ); Vector2f tPos( pos.x + size.getWidth() - rightW, pos.y + rightH ); - if ( rightW > borderTop ) { - vbo->addVertex( - Vector2f( tPos.x + ( rightW - borderTop ) * Math::cosAng( startAngle ), - tPos.y + ( rightH - borderTop ) * Math::sinAng( startAngle ) ) ); + if ( rightW > borderRight || rightH > borderTop ) { + vbo->addVertex( Vector2f( + tPos.x + eemax( 0., rightW - borderRight ) * Math::cosAng( startAngle ), + tPos.y + ( rightH - borderTop ) * Math::sinAng( startAngle ) ) ); } else { vbo->addVertex( basePos ); } @@ -296,7 +328,7 @@ void Borders::createBorders( VertexBuffer* vbo, const Borders& borders, const Ve vbo->addColor( borders.top.color ); borderAddArc( vbo, tPos, rightW, rightH, startAngle, endAngle, borders.top.color, - borderTop, basePos ); + borderRight, borderTop, basePos ); lastOuter = arcOuterPos( tPos, rightW, rightH, endAngle ); } else { @@ -329,7 +361,8 @@ void Borders::createBorders( VertexBuffer* vbo, const Borders& borders, const Ve Vector2f basePos( pos.x + size.getWidth() - borderRight, pos.y + borderTop ); borderAddArc( vbo, Vector2f( pos.x + size.getWidth() - topW, pos.y + topH ), topW, topH, - startAngle, endAngle, borders.right.color, borderRight, basePos ); + startAngle, endAngle, borders.right.color, borderRight, borderTop, + basePos ); } else { vbo->addVertex( Vector2f( pos.x + size.getWidth() - borderRight, pos.y + borderTop ) ); vbo->addColor( borders.right.color ); @@ -344,10 +377,10 @@ void Borders::createBorders( VertexBuffer* vbo, const Borders& borders, const Ve pos.y + size.getHeight() - borderBottom ); Vector2f tPos( pos.x + size.getWidth() - bottomW, pos.y + size.getHeight() - bottomH ); - if ( bottomW > borderRight ) { - vbo->addVertex( - Vector2f( tPos.x + ( bottomW - borderRight ) * Math::cosAng( startAngle ), - tPos.y + ( bottomH - borderRight ) * Math::sinAng( startAngle ) ) ); + if ( bottomW > borderRight || bottomH > borderBottom ) { + vbo->addVertex( Vector2f( + tPos.x + ( bottomW - borderRight ) * Math::cosAng( startAngle ), + tPos.y + eemax( 0., bottomH - borderBottom ) * Math::sinAng( startAngle ) ) ); } else { vbo->addVertex( basePos ); } @@ -358,7 +391,7 @@ void Borders::createBorders( VertexBuffer* vbo, const Borders& borders, const Ve vbo->addColor( borders.right.color ); borderAddArc( vbo, tPos, bottomW, bottomH, startAngle, endAngle, borders.right.color, - borderRight, basePos ); + borderRight, borderBottom, basePos ); lastOuter = arcOuterPos( tPos, bottomW, bottomH, endAngle ); } else { @@ -391,7 +424,8 @@ void Borders::createBorders( VertexBuffer* vbo, const Borders& borders, const Ve borderAddArc( vbo, Vector2f( pos.x + size.getWidth() - rightW, pos.y + size.getHeight() - rightH ), - rightW, rightH, startAngle, endAngle, borders.bottom.color, borderBottom, basePos ); + rightW, rightH, startAngle, endAngle, borders.bottom.color, borderRight, + borderBottom, basePos ); } else { vbo->addVertex( Vector2f( pos.x + size.getWidth() - borderRight, pos.y + size.getHeight() - borderBottom ) ); @@ -406,9 +440,9 @@ void Borders::createBorders( VertexBuffer* vbo, const Borders& borders, const Ve Vector2f basePos( pos.x + borderLeft, pos.y + size.getHeight() - borderBottom ); Vector2f tPos( Vector2f( pos.x + leftW, pos.y + size.getHeight() - leftH ) ); - if ( leftW > borderBottom ) { + if ( leftW > borderLeft || leftH > borderBottom ) { vbo->addVertex( - Vector2f( tPos.x + ( leftW - borderBottom ) * Math::cosAng( startAngle ), + Vector2f( tPos.x + eemax( 0., leftW - borderLeft ) * Math::cosAng( startAngle ), tPos.y + ( leftH - borderBottom ) * Math::sinAng( startAngle ) ) ); } else { vbo->addVertex( basePos ); @@ -421,7 +455,7 @@ void Borders::createBorders( VertexBuffer* vbo, const Borders& borders, const Ve vbo->addColor( borders.bottom.color ); borderAddArc( vbo, tPos, leftW, leftH, startAngle, endAngle, borders.bottom.color, - borderBottom, basePos ); + borderLeft, borderBottom, basePos ); lastOuter = arcOuterPos( tPos, leftW, leftH, endAngle ); } else { @@ -452,7 +486,7 @@ void Borders::createBorders( VertexBuffer* vbo, const Borders& borders, const Ve borderAddArc( vbo, Vector2f( pos.x + bottomW, pos.y + size.getHeight() - bottomH ), bottomW, bottomH, startAngle, endAngle, borders.left.color, borderLeft, - basePos ); + borderBottom, basePos ); } else { vbo->addVertex( Vector2f( pos.x + borderLeft, pos.y + size.getHeight() - borderBottom ) ); @@ -467,10 +501,10 @@ void Borders::createBorders( VertexBuffer* vbo, const Borders& borders, const Ve Vector2f basePos( pos.x + borderLeft, pos.y + borderTop ); Vector2f tPos( pos.x + topW, pos.y + topH ); - if ( topW > borderLeft ) { - vbo->addVertex( - Vector2f( tPos.x + ( topW - borderLeft ) * Math::cosAng( startAngle ), - tPos.y + ( topH - borderLeft ) * Math::sinAng( startAngle ) ) ); + if ( topW > borderLeft || topH > borderTop ) { + vbo->addVertex( Vector2f( + tPos.x + ( topW - borderLeft ) * Math::cosAng( startAngle ), + tPos.y + eemax( 0., topH - borderTop ) * Math::sinAng( startAngle ) ) ); } else { vbo->addVertex( basePos ); } @@ -483,7 +517,7 @@ void Borders::createBorders( VertexBuffer* vbo, const Borders& borders, const Ve vbo->addColor( borders.left.color ); borderAddArc( vbo, tPos, topW, topH, startAngle, endAngle, borders.left.color, - borderLeft, basePos ); + borderLeft, borderTop, basePos ); } else { vbo->addVertex( Vector2f( pos.x + borderLeft, pos.y + borderTop ) ); vbo->addColor( borders.left.color ); @@ -497,56 +531,55 @@ void Borders::createBackground( VertexBuffer* vbo, const BorderRadiuses& radius, const Vector2f& pos, const Sizef& size, const Color& color ) { vbo->clear(); - Float halfWidth = size.getWidth() * 0.5f; - Float halfHeight = size.getHeight() * 0.5f; + const BorderRadiuses usedRadius = normalizeRadiuses( radius, size ); - double leftH = eemin( halfHeight, eemax( 0.f, radius.topLeft.y ) ); - if ( radius.topLeft.x > 0 ) { - double leftW = eemin( halfWidth, eemax( 0.f, radius.topLeft.x ) ); + double leftH = usedRadius.topLeft.y; + if ( usedRadius.topLeft.x > 0 ) { + double leftW = usedRadius.topLeft.x; double startAngle = 180; double endAngle = 270; borderAddArc( vbo, Vector2f( pos.x + leftW, pos.y + leftH ), leftW, leftH, startAngle, - endAngle, color, 0, Vector2f::Zero, false, false ); + endAngle, color, 0, 0, Vector2f::Zero, false, false ); } else { vbo->addVertex( Vector2f( pos.x, pos.y ) ); vbo->addColor( color ); } - double rightW = eemin( halfWidth, eemax( 0.f, radius.topRight.x ) ); + double rightW = usedRadius.topRight.x; vbo->addVertex( Vector2f( pos.x + size.getWidth() - rightW, pos.y ) ); vbo->addColor( color ); - if ( radius.topRight.x > 0 ) { - double rightH = eemin( halfHeight, eemax( 0.f, radius.topRight.y ) ); + if ( usedRadius.topRight.x > 0 ) { + double rightH = usedRadius.topRight.y; double startAngle = 270; double endAngle = 360; borderAddArc( vbo, Vector2f( pos.x + size.getWidth() - rightW, pos.y + rightH ), rightW, - rightH, startAngle, endAngle, color, 0, Vector2f::Zero, false, false ); + rightH, startAngle, endAngle, color, 0, 0, Vector2f::Zero, false, false ); } - double bottomH = eemin( halfHeight, eemax( 0.f, radius.bottomRight.y ) ); + double bottomH = usedRadius.bottomRight.y; vbo->addVertex( Vector2f( pos.x + size.getWidth(), pos.y + size.getHeight() - bottomH ) ); vbo->addColor( color ); - if ( radius.bottomRight.x > 0 ) { - double bottomW = eemin( halfWidth, eemax( 0.f, radius.bottomRight.x ) ); + if ( usedRadius.bottomRight.x > 0 ) { + double bottomW = usedRadius.bottomRight.x; double startAngle = 0; double endAngle = 90; borderAddArc( vbo, Vector2f( pos.x + size.getWidth() - bottomW, pos.y + size.getHeight() - bottomH ), - bottomW, bottomH, startAngle, endAngle, color, 0, Vector2f::Zero, false, false ); + bottomW, bottomH, startAngle, endAngle, color, 0, 0, Vector2f::Zero, false, false ); } - double bottomW = eemin( halfWidth, eemax( 0.f, radius.bottomLeft.x ) ); + double bottomW = usedRadius.bottomLeft.x; vbo->addVertex( Vector2f( pos.x + bottomW, pos.y + size.getHeight() ) ); vbo->addColor( color ); - if ( radius.bottomLeft.x > 0 ) { - double bottomH = eemin( halfHeight, eemax( 0.f, radius.bottomLeft.y ) ); + if ( usedRadius.bottomLeft.x > 0 ) { + double bottomH = usedRadius.bottomLeft.y; double startAngle = 90; double endAngle = 180; borderAddArc( vbo, Vector2f( pos.x + bottomW, pos.y + size.getHeight() - bottomH ), bottomW, - bottomH, startAngle, endAngle, color, 0, Vector2f::Zero, false, false ); + bottomH, startAngle, endAngle, color, 0, 0, Vector2f::Zero, false, false ); } vbo->addVertex( Vector2f( pos.x, pos.y + leftH ) ); diff --git a/src/eepp/ui/uihtmlinput.cpp b/src/eepp/ui/uihtmlinput.cpp index 7424285b4..b364032f3 100644 --- a/src/eepp/ui/uihtmlinput.cpp +++ b/src/eepp/ui/uihtmlinput.cpp @@ -25,6 +25,65 @@ static bool htmlBoolAttributeIsTrue( const StyleSheetProperty& property ) { return property.asBool(); } +static std::string normalizeInputType( std::string type ) { + type = String::toLower( String::trim( type ) ); + if ( type == "button" || type == "checkbox" || type == "color" || type == "date" || + type == "datetime-local" || type == "email" || type == "file" || type == "hidden" || + type == "image" || type == "month" || type == "number" || type == "password" || + type == "radio" || type == "range" || type == "reset" || type == "search" || + type == "submit" || type == "tel" || type == "text" || type == "time" || type == "url" || + type == "week" ) + return type; + return "text"; +} + +static bool isImplementationProperty( PropertyId id ) { + switch ( id ) { + case PropertyId::Size: + case PropertyId::MaxLength: + case PropertyId::AllowEditing: + case PropertyId::Numeric: + case PropertyId::AllowFloat: + case PropertyId::InputMode: + case PropertyId::Hint: + case PropertyId::HintColor: + case PropertyId::HintShadowColor: + case PropertyId::HintShadowOffset: + case PropertyId::HintFontFamily: + case PropertyId::HintFontSize: + case PropertyId::HintFontStyle: + case PropertyId::HintStrokeWidth: + case PropertyId::HintStrokeColor: + case PropertyId::HintDisplay: + case PropertyId::MinValue: + case PropertyId::MaxValue: + case PropertyId::ClickStep: + return true; + default: + return false; + } +} + +static bool implementationPropertyAffectsIntrinsicSize( PropertyId id ) { + switch ( id ) { + case PropertyId::Size: + case PropertyId::FontFamily: + case PropertyId::FontSize: + case PropertyId::FontStyle: + case PropertyId::FontWeight: + return true; + default: + return false; + } +} + +static void markAnonymousControlTree( Node* node ) { + if ( node->isWidget() ) + node->asType()->setFlags( UI_IGNORE_GLOBAL_CSS ); + for ( Node* child = node->getFirstChild(); child; child = child->getNextNode() ) + markAnonymousControlTree( child ); +} + } // namespace UIHTMLInput* UIHTMLInput::New() { @@ -57,13 +116,14 @@ bool UIHTMLInput::applyProperty( const StyleSheetProperty& attribute ) { case PropertyId::Checked: case PropertyId::Selected: mChecked = htmlBoolAttributeIsTrue( attribute ); - mProperties[id] = attribute; syncCheckedState(); return UIHTMLWidget::applyProperty( attribute ); case PropertyId::Value: case PropertyId::Text: mValue = attribute.value(); - break; + if ( mChildWidget && !( mInputType == "checkbox" || mInputType == "radio" ) ) + mChildWidget->applyProperty( attribute ); + return UIHTMLWidget::applyProperty( attribute ); case PropertyId::Type: setInputType( attribute.value() ); return true; @@ -71,13 +131,10 @@ bool UIHTMLInput::applyProperty( const StyleSheetProperty& attribute ) { break; } - if ( id != PropertyId::Id && id != PropertyId::Class && id != PropertyId::Type && - id != PropertyId::Display ) { - mProperties[id] = attribute; - if ( mChildWidget && !( id == PropertyId::Value && - ( mInputType == "checkbox" || mInputType == "radio" ) ) ) { - mChildWidget->applyProperty( attribute ); - } + if ( isImplementationProperty( id ) || attribute.getPropertyDefinition()->isInherited() ) { + mImplementationProperties[id] = attribute; + if ( mChildWidget ) + applyImplementationProperty( attribute ); } return UIHTMLWidget::applyProperty( attribute ); @@ -125,13 +182,26 @@ Float UIHTMLInput::getMaxIntrinsicWidth() const { return mChildWidget ? mChildWidget->getMaxIntrinsicWidth() : 0; } +void UIHTMLInput::updateLayout() { + // An input is a replaced element. Its anonymous native control must not be processed as a DOM + // child by BlockLayouter, which would restore the control's intrinsic size after flex/grid had + // assigned the host's final used size. + positionOutOfFlowChildren(); + if ( isOutOfFlow() ) + updateOutOfFlowPosition(); + mDirtyLayout = false; + updateChildGeometry(); +} + const std::string& UIHTMLInput::getInputType() const { return mInputType; } void UIHTMLInput::setInputType( const std::string& type ) { - if ( mInputType != type ) { - mInputType = type; + const std::string normalizedType = normalizeInputType( type ); + if ( mInputType != normalizedType ) { + syncStateFromImplementation(); + mInputType = normalizedType; createChildWidget(); } } @@ -151,9 +221,14 @@ void UIHTMLInput::createChildWidget() { } else if ( mInputType == "checkbox" ) { mChildWidget = UICheckBox::New(); } else if ( mInputType == "hidden" ) { + if ( !mHiddenByType ) { + mVisibleBeforeHidden = isVisible(); + mEnabledBeforeHidden = isEnabled(); + mDisplayBeforeHidden = mDisplay; + } + mHiddenByType = true; setVisible( false ); setEnabled( false ); - mVisible = false; mDisplay = CSSDisplay::None; } else if ( mInputType == "number" ) { mChildWidget = UISpinBox::New(); @@ -168,27 +243,120 @@ void UIHTMLInput::createChildWidget() { if ( mChildWidget == nullptr ) return; - mChildWidget->setFlags( UI_HTML_ELEMENT ); + if ( mHiddenByType ) { + mHiddenByType = false; + mDisplay = mDisplayBeforeHidden; + setEnabled( mEnabledBeforeHidden ); + setVisible( mVisibleBeforeHidden ); + } - if ( mChildWidget ) { - mChildWidget->setParent( this ); + configureChildWidget(); + syncImplementationState(); +} + +void UIHTMLInput::configureChildWidget() { + mChildWidget->setParent( this ); + markAnonymousControlTree( mChildWidget ); + mChildWidget->setLayoutWidthPolicy( SizePolicy::WrapContent ); + mChildWidget->setLayoutHeightPolicy( SizePolicy::WrapContent ); + + // The child is anonymous control content, not a second HTML/CSS box. The host owns author + // backgrounds, borders and padding; native subparts (check marks, spin buttons, etc.) remain. + mChildWidget->removeSkin(); + mChildWidget->setBackgroundFillEnabled( false ); + mChildWidget->setBorderEnabled( false ); + mChildWidget->unsetFlags( UI_AUTO_PADDING ); + mChildWidget->setPadding( Rectf() ); + + mChildWidget->on( Event::OnSizeChange, [this]( auto ) { + if ( !mChildWidget || mSyncingGeometry ) + return; + invalidateIntrinsicSize(); + updateHostGeometry(); + notifyLayoutAttrChangeParent( LayoutInvalidation::ParentChildChange ); + } ); +} + +void UIHTMLInput::applyImplementationProperty( const StyleSheetProperty& property ) { + if ( !mChildWidget ) + return; + + const bool remeasure = implementationPropertyAffectsIntrinsicSize( + property.getPropertyDefinition()->getPropertyId() ); + if ( remeasure ) { mChildWidget->setLayoutWidthPolicy( SizePolicy::WrapContent ); mChildWidget->setLayoutHeightPolicy( SizePolicy::WrapContent ); - mChildWidget->on( Event::OnSizeChange, [this]( auto ) { - if ( mChildWidget ) - setPixelsSize( mChildWidget->getPixelsSize() ); - } ); - - for ( const auto& propIt : mProperties ) { - if ( propIt.first == PropertyId::Checked || propIt.first == PropertyId::Selected ) - continue; - if ( propIt.first == PropertyId::Value && - ( mInputType == "checkbox" || mInputType == "radio" ) ) - continue; - mChildWidget->applyProperty( propIt.second ); - } - syncCheckedState(); } + mChildWidget->applyProperty( property ); + if ( remeasure ) { + updateHostGeometry(); + mChildWidget->setLayoutWidthPolicy( SizePolicy::Fixed ); + mChildWidget->setLayoutHeightPolicy( SizePolicy::Fixed ); + updateChildGeometry(); + } +} + +void UIHTMLInput::syncImplementationState() { + if ( !mChildWidget ) + return; + + for ( const auto& prop : mImplementationProperties ) + mChildWidget->applyProperty( prop.second ); + + if ( !( mInputType == "checkbox" || mInputType == "radio" ) ) + mChildWidget->applyProperty( StyleSheetProperty( "value", mValue ) ); + syncCheckedState(); + updateHostGeometry(); + mChildWidget->setLayoutWidthPolicy( SizePolicy::Fixed ); + mChildWidget->setLayoutHeightPolicy( SizePolicy::Fixed ); + updateChildGeometry(); +} + +void UIHTMLInput::syncStateFromImplementation() { + if ( !mChildWidget ) + return; + + if ( mInputType == "checkbox" ) { + mChecked = static_cast( mChildWidget )->isChecked(); + } else if ( mInputType == "radio" ) { + mChecked = static_cast( mChildWidget )->isActive(); + } else { + mValue = getFormValue(); + } +} + +void UIHTMLInput::updateHostGeometry() { + if ( !mChildWidget || mSyncingGeometry ) + return; + + const Rectf contentOffset = getPixelsContentOffset(); + Sizef size = getPixelsSize(); + if ( getLayoutWidthPolicy() == SizePolicy::WrapContent ) + size.setWidth( mChildWidget->getPixelsSize().getWidth() + contentOffset.Left + + contentOffset.Right ); + if ( getLayoutHeightPolicy() == SizePolicy::WrapContent ) + size.setHeight( mChildWidget->getPixelsSize().getHeight() + contentOffset.Top + + contentOffset.Bottom ); + + mSyncingGeometry = true; + setPixelsSize( size ); + mSyncingGeometry = false; + updateChildGeometry(); +} + +void UIHTMLInput::updateChildGeometry() { + if ( !mChildWidget || mSyncingGeometry ) + return; + + const Rectf contentOffset = getPixelsContentOffset(); + const Sizef contentSize( + eemax( 0.f, getPixelsSize().getWidth() - contentOffset.Left - contentOffset.Right ), + eemax( 0.f, getPixelsSize().getHeight() - contentOffset.Top - contentOffset.Bottom ) ); + mSyncingGeometry = true; + mChildWidget->setPixelsPosition( contentOffset.Left, contentOffset.Top ); + if ( contentSize.getWidth() > 0 && contentSize.getHeight() > 0 ) + mChildWidget->setPixelsSize( contentSize ); + mSyncingGeometry = false; } void UIHTMLInput::syncCheckedState() { @@ -227,6 +395,12 @@ String UIHTMLInput::getFormValue() const { void UIHTMLInput::onSizeChange() { UIHTMLWidget::onSizeChange(); + updateChildGeometry(); +} + +void UIHTMLInput::onPaddingChange() { + UIHTMLWidget::onPaddingChange(); + updateHostGeometry(); } }} // namespace EE::UI diff --git a/src/eepp/ui/uistyle.cpp b/src/eepp/ui/uistyle.cpp index 4f2500914..63cd2d774 100644 --- a/src/eepp/ui/uistyle.cpp +++ b/src/eepp/ui/uistyle.cpp @@ -74,6 +74,12 @@ void UIStyle::setStyleSheetVariable( const StyleSheetVariable& variable ) { void UIStyle::resetGlobalDefinition( bool force ) { const auto& stylesheet = mWidget->getUISceneNode()->getStyleSheet(); + if ( mWidget->getFlags() & UI_IGNORE_GLOBAL_CSS ) { + mGlobalDefinition = nullptr; + mLoadedStyleSheet = &stylesheet; + mLoadedVersion = stylesheet.getVersion(); + return; + } if ( !force && &stylesheet == mLoadedStyleSheet && stylesheet.getVersion() == mLoadedVersion ) return; diff --git a/src/tests/unit_tests/border_tests.cpp b/src/tests/unit_tests/border_tests.cpp new file mode 100644 index 000000000..588c0653f --- /dev/null +++ b/src/tests/unit_tests/border_tests.cpp @@ -0,0 +1,85 @@ +#include "utest.h" + +#include + +using namespace EE; +using namespace EE::UI; + +static BorderRadiuses equalRadiuses( Float x, Float y ) { + return { { x, y }, { x, y }, { x, y }, { x, y } }; +} + +#define EXPECT_RADIUS( radiusValue, expectedX, expectedY ) \ + do { \ + EXPECT_NEAR( expectedX, radiusValue.x, 0.001f ); \ + EXPECT_NEAR( expectedY, radiusValue.y, 0.001f ); \ + } while ( false ) + +UTEST( Borders, NormalizeEqualCircularRadiuses ) { + const BorderRadiuses input = equalRadiuses( 9999.f, 9999.f ); + const auto square = Borders::normalizeRadiuses( input, { 100.f, 100.f } ); + const auto capsule = Borders::normalizeRadiuses( input, { 200.f, 100.f } ); + + EXPECT_RADIUS( square.topLeft, 50.f, 50.f ); + EXPECT_RADIUS( square.topRight, 50.f, 50.f ); + EXPECT_RADIUS( square.bottomRight, 50.f, 50.f ); + EXPECT_RADIUS( square.bottomLeft, 50.f, 50.f ); + EXPECT_RADIUS( capsule.topLeft, 50.f, 50.f ); + EXPECT_RADIUS( capsule.topRight, 50.f, 50.f ); + EXPECT_RADIUS( capsule.bottomRight, 50.f, 50.f ); + EXPECT_RADIUS( capsule.bottomLeft, 50.f, 50.f ); +} + +UTEST( Borders, NormalizeAsymmetricHorizontalRadiusesProportionally ) { + BorderRadiuses input; + input.topLeft = { 80.f, 12.f }; + input.topRight = { 40.f, 6.f }; + + const auto used = Borders::normalizeRadiuses( input, { 100.f, 100.f } ); + EXPECT_RADIUS( used.topLeft, 66.6667f, 10.f ); + EXPECT_RADIUS( used.topRight, 33.3333f, 5.f ); +} + +UTEST( Borders, NormalizeUsesSmallestEdgeConstraintForEveryComponent ) { + BorderRadiuses bottomLimited = { { 10.f, 5.f }, { 20.f, 6.f }, { 60.f, 8.f }, { 90.f, 7.f } }; + const auto bottom = Borders::normalizeRadiuses( bottomLimited, { 100.f, 100.f } ); + EXPECT_RADIUS( bottom.topLeft, 100.f / 15.f, 10.f / 3.f ); + EXPECT_RADIUS( bottom.bottomLeft, 40.f, 16.f / 3.f ); + + BorderRadiuses leftLimited = { { 12.f, 60.f }, { 8.f, 10.f }, { 4.f, 90.f }, { 6.f, 20.f } }; + const auto left = Borders::normalizeRadiuses( leftLimited, { 200.f, 100.f } ); + EXPECT_RADIUS( left.topRight, 16.f / 3.f, 20.f / 3.f ); + EXPECT_RADIUS( left.bottomLeft, 8.f / 3.f, 60.f ); + + BorderRadiuses rightLimited = { { 5.f, 10.f }, { 10.f, 80.f }, { 15.f, 20.f }, { 20.f, 40.f } }; + const auto right = Borders::normalizeRadiuses( rightLimited, { 200.f, 100.f } ); + EXPECT_RADIUS( right.topLeft, 25.f / 6.f, 25.f / 3.f ); + EXPECT_RADIUS( right.topRight, 25.f / 3.f, 200.f / 3.f ); +} + +UTEST( Borders, NormalizePreservesEllipticalAspectRatio ) { + const BorderRadiuses input = equalRadiuses( 120.f, 30.f ); + const auto used = Borders::normalizeRadiuses( input, { 100.f, 200.f } ); + EXPECT_RADIUS( used.topLeft, 50.f, 12.5f ); + EXPECT_NEAR( input.topLeft.x / input.topLeft.y, used.topLeft.x / used.topLeft.y, 0.001f ); +} + +UTEST( Borders, NormalizeHandlesZeroAndNegativeRadiuses ) { + BorderRadiuses input; + input.topLeft = { -10.f, -20.f }; + const auto used = Borders::normalizeRadiuses( input, { 100.f, 100.f } ); + EXPECT_RADIUS( used.topLeft, 0.f, 0.f ); + EXPECT_RADIUS( used.topRight, 0.f, 0.f ); +} + +UTEST( Borders, NormalizeResizeStartsFromSpecifiedRadiuses ) { + const BorderRadiuses specified = equalRadiuses( 80.f, 80.f ); + const auto small = Borders::normalizeRadiuses( specified, { 100.f, 100.f } ); + const auto large = Borders::normalizeRadiuses( specified, { 200.f, 200.f } ); + + EXPECT_RADIUS( small.topLeft, 50.f, 50.f ); + EXPECT_RADIUS( large.topLeft, 80.f, 80.f ); + EXPECT_RADIUS( specified.topLeft, 80.f, 80.f ); +} + +#undef EXPECT_RADIUS diff --git a/src/tests/unit_tests/uihtml_tests.cpp b/src/tests/unit_tests/uihtml_tests.cpp index 46eb4c2fc..2fb357659 100644 --- a/src/tests/unit_tests/uihtml_tests.cpp +++ b/src/tests/unit_tests/uihtml_tests.cpp @@ -1467,6 +1467,7 @@ UTEST( UIHTMLTable, nestedSpecifiedWidth ) { UTEST( UIHTMLInput, sizeAttribute ) { init_ui_test(); auto* sceneNode = SceneManager::instance()->getUISceneNode(); + sceneNode->combineStyleSheet( "* { background-color: #fedcba; border-width: 2px; }" ); sceneNode->loadLayoutFromString( R"html( @@ -1969,6 +1970,77 @@ UTEST( UIHTML, FormControlsDefaultInlineBlock ) { Engine::destroySingleton(); } +UTEST( UIHTMLInput, hostOwnsCSSBox ) { + init_ui_test(); + auto* sceneNode = SceneManager::instance()->getUISceneNode(); + sceneNode->loadLayoutFromString( R"html( + + + + )html" ); + sceneNode->updateDirtyLayouts(); + + auto* input = sceneNode->getRoot()->find( "styled_input" )->asType(); + ASSERT_TRUE( input != nullptr ); + auto* implementation = input->getChildWidget(); + ASSERT_TRUE( implementation != nullptr ); + + EXPECT_TRUE( input->hasBackground() ); + EXPECT_TRUE( input->hasBorder() ); + EXPECT_FALSE( implementation->hasBackground() ); + EXPECT_FALSE( implementation->hasBorder() ); + EXPECT_TRUE( 0 == ( implementation->getFlags() & UI_HTML_ELEMENT ) ); + EXPECT_NEAR( input->getPixelsPadding().Left, 5.f, 0.01f ); + EXPECT_NEAR( input->getPixelsPadding().Top, 5.f, 0.01f ); + EXPECT_NEAR( input->getPixelsPadding().Right, 5.f, 0.01f ); + EXPECT_NEAR( input->getPixelsPadding().Bottom, 5.f, 0.01f ); + EXPECT_TRUE( implementation->getPadding() == Rectf() ); + EXPECT_NEAR( implementation->getPixelsPosition().x, 8.f, 0.01f ); + EXPECT_NEAR( implementation->getPixelsPosition().y, 8.f, 0.01f ); + EXPECT_NEAR( implementation->getPixelsSize().getWidth(), + input->getPixelsSize().getWidth() - 16.f, 0.01f ); + EXPECT_NEAR( implementation->getPixelsSize().getHeight(), + input->getPixelsSize().getHeight() - 16.f, 0.01f ); + + input->setInputType( "email" ); + EXPECT_STDSTREQ( input->getInputType(), "email" ); + EXPECT_TRUE( input->getChildWidget()->isType( UI_TYPE_TEXTINPUT ) ); + + Engine::destroySingleton(); +} + +UTEST( UIHTMLInput, dynamicTypeKeepsHostAndState ) { + init_ui_test(); + auto* input = UIHTMLInput::New(); + input->setParent( SceneManager::instance()->getUISceneNode()->getRoot() ); + static_cast( input->getChildWidget() )->setText( "edited value" ); + + UIHTMLInput* originalHost = input; + input->setInputType( "checkbox" ); + EXPECT_EQ( input, originalHost ); + EXPECT_TRUE( input->getChildWidget()->isType( UI_TYPE_CHECKBOX ) ); + + input->setInputType( "hidden" ); + EXPECT_FALSE( input->isVisible() ); + EXPECT_FALSE( input->isEnabled() ); + EXPECT_EQ( input->getDisplay(), CSSDisplay::None ); + + input->setInputType( "text" ); + EXPECT_EQ( input, originalHost ); + EXPECT_TRUE( input->isVisible() ); + EXPECT_TRUE( input->isEnabled() ); + EXPECT_EQ( input->getDisplay(), CSSDisplay::InlineBlock ); + ASSERT_TRUE( input->getChildWidget()->isType( UI_TYPE_TEXTINPUT ) ); + EXPECT_TRUE( static_cast( input->getChildWidget() )->getText() == + "edited value" ); + + input->setInputType( "unsupported-type" ); + EXPECT_STDSTREQ( input->getInputType(), "text" ); + + Engine::destroySingleton(); +} + UTEST( UIHTMLTable, tableLayoutFixed ) { Engine::instance()->createWindow( WindowSettings( 1024, 650, "HTML Tables Test", WindowStyle::Default, WindowBackend::Default, @@ -4886,6 +4958,26 @@ UTEST( UIHTML, FlexMediaQueriesLayout ) { EXPECT_GT( essayNavWidget->getPixelsSize().getHeight(), 10.f ); EXPECT_GT( essayNavWidget->getPixelsSize().getWidth(), 10.f ); + // Valid input types that use the text implementation must retain their state for attribute + // selectors. Collapsing "email" to "text" loses this rule and exposes the white UA default. + auto* newsletter = bodyWidget->findByClass( "newsletter-form" ); + ASSERT_TRUE( newsletter != nullptr ); + auto* emailInput = newsletter->findByTag( "input" )->asType(); + ASSERT_TRUE( emailInput != nullptr ); + EXPECT_STDSTREQ( emailInput->getInputType(), "email" ); + EXPECT_TRUE( emailInput->getBackgroundColor() == Color( "#1C1917" ) ); + auto* emailImplementation = emailInput->getChildWidget(); + ASSERT_TRUE( emailImplementation != nullptr ); + const Rectf emailContentOffset = emailInput->getPixelsContentOffset(); + EXPECT_NEAR( emailImplementation->getPixelsSize().getWidth(), + emailInput->getPixelsSize().getWidth() - emailContentOffset.Left - + emailContentOffset.Right, + 0.01f ); + EXPECT_NEAR( emailImplementation->getPixelsSize().getHeight(), + emailInput->getPixelsSize().getHeight() - emailContentOffset.Top - + emailContentOffset.Bottom, + 0.01f ); + // The essay-nav link contains two spans: label and title // They should stack vertically (flex-direction: column on the ) // so the link height should be at least the sum of both span heights