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.
This commit is contained in:
Martín Lucas Golini
2026-07-27 11:17:23 -03:00
parent 90f41a64aa
commit 935b28c91a
10 changed files with 522 additions and 113 deletions
+115 -82
View File
@@ -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 ) );
+203 -29
View File
@@ -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<UIWidget>()->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<UICheckBox*>( mChildWidget )->isChecked();
} else if ( mInputType == "radio" ) {
mChecked = static_cast<UIRadioButton*>( 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
+6
View File
@@ -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;
+85
View File
@@ -0,0 +1,85 @@
#include "utest.h"
#include <eepp/ui/border.hpp>
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
+92
View File
@@ -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(
<vbox layout_width="wrap_content" layout_height="wrap_content">
<input id="i1" size="10" />
@@ -1969,6 +1970,77 @@ UTEST( UIHTML, FormControlsDefaultInlineBlock ) {
Engine::destroySingleton();
}
UTEST( UIHTMLInput, hostOwnsCSSBox ) {
init_ui_test();
auto* sceneNode = SceneManager::instance()->getUISceneNode();
sceneNode->loadLayoutFromString( R"html(
<vbox layout_width="wrap_content" layout_height="wrap_content">
<input id="styled_input" type="text"
style="background-color: #123456; border: 3px solid #abcdef; padding: 5px;" />
</vbox>
)html" );
sceneNode->updateDirtyLayouts();
auto* input = sceneNode->getRoot()->find( "styled_input" )->asType<UIHTMLInput>();
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<UITextInput*>( 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<UITextInput*>( 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<UIHTMLInput>();
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 <a>)
// so the link height should be at least the sum of both span heights