From 935b28c91abde25e25df7a39627226d72400114d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=ADn=20Lucas=20Golini?= Date: Mon, 27 Jul 2026 11:17:23 -0300 Subject: [PATCH] Fix HTML input styling and internal control layout Treat UIHTMLInput as the stable HTML/CSS host while keeping its native control as an anonymous implementation detail. Prevent author CSS from matching internal control widgets, avoiding duplicated backgrounds and other styles. Keep valid input types such as email intact for attribute selectors while falling back to compatible native implementations. Synchronize the internal control with the host content box after layout, including flex sizing, and preserve intrinsic measurement for size and font changes. Preserve input state across dynamic type changes and make hidden transitions reversible. Also fix border-radius calculations and add regressions for CSS ownership, anonymous widget isolation, dynamic type changes, input sizing, and the Causality newsletter form. --- .../html/eepp-ui-border-rendering-2.webp | Bin 3684 -> 3700 bytes .../eepp-uihtmltable-complex-layout-2.webp | Bin 27464 -> 27508 bytes include/eepp/ui/border.hpp | 4 + include/eepp/ui/uihelper.hpp | 3 +- include/eepp/ui/uihtmlinput.hpp | 16 +- src/eepp/ui/border.cpp | 197 ++++++++------- src/eepp/ui/uihtmlinput.cpp | 232 +++++++++++++++--- src/eepp/ui/uistyle.cpp | 6 + src/tests/unit_tests/border_tests.cpp | 85 +++++++ src/tests/unit_tests/uihtml_tests.cpp | 92 +++++++ 10 files changed, 522 insertions(+), 113 deletions(-) create mode 100644 src/tests/unit_tests/border_tests.cpp 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 8f3ae1caf47e60ad0014b8492052dcd02027d8c6..a0ea9a315e34d4941dc99ec92f4e8f8003d8e4cc 100644 GIT binary patch delta 3262 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 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 f06d8b567990996a12b360d232870408ba8fd34a..56fdbadce8299352caae8e4bbde1e28fda93c19c 100644 GIT binary patch delta 26796 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 F{{h(PLc0I} delta 26845 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 sE*Ev}JlC{&GWFRq!wZ3UdXo=-|9L}jBxis{jB1 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