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