From f765eae28dd69b91ae105261de84a7998f2b438e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=ADn=20Lucas=20Golini?= Date: Tue, 28 Apr 2026 21:52:31 -0300 Subject: [PATCH] Greatly improve borders rendering, added a few tests. Fixed `UIStyle::getProperty`, now respects specificity. Fixed `StyleSheetPropertiesParser::addProperty` when inserted an already existing property. --- bin/unit_tests/assets/html/border_tests.html | 171 +++++++++++++++ .../assets/html/eepp-ui-border-rendering.webp | Bin 0 -> 7522 bytes src/eepp/ui/border.cpp | 194 +++++++++++++++--- .../ui/css/stylesheetpropertiesparser.cpp | 4 +- src/eepp/ui/uistyle.cpp | 11 +- src/eepp/ui/uiwidget.cpp | 16 +- src/tests/unit_tests/uihtml_tests.cpp | 34 +++ 7 files changed, 382 insertions(+), 48 deletions(-) create mode 100644 bin/unit_tests/assets/html/border_tests.html create mode 100644 bin/unit_tests/assets/html/eepp-ui-border-rendering.webp diff --git a/bin/unit_tests/assets/html/border_tests.html b/bin/unit_tests/assets/html/border_tests.html new file mode 100644 index 000000000..7bbebcb2c --- /dev/null +++ b/bin/unit_tests/assets/html/border_tests.html @@ -0,0 +1,171 @@ + + + + + + + + + +
+
1 border
+
top
+
right
+
bottom
+
left
+
+ +
+
1 border 8px
+
top
+
right
+
bottom
+
left
+
+ + +
+
2 adj borders
+
top+right
+
right+bot
+
bot+left
+
left+top
+
+ +
+
2 adj 2px/8px
+
t2/r8
+
r8/b2
+
b2/l8
+
l8/t2
+
+ + +
+
2 opp borders
+
top+bot
+
left+right
+
t8/b2
+
l2/r8
+
+ + +
+
3 borders
+
no left
+
no top
+
no right
+
no bottom
+
+ +
+
3 brdrs mixed
+
t8/r2/b4
+
r2/b8/l4
+
b4/l8/t2
+
l2/t4/r8
+
+ + +
+
4 borders
+
all 4px
+
all 1px
+
all 8px
+
mixed
+
+ +
+
4 brdrs colors
+
orange
+
teal
+
purple
+
multi
+
+ + +
+
radius all same
+
r4
+
r8
+
r12
+
r20
+
+ +
+
radius diff
+
tl+br
+
tr+bl
+
mixed
+
tl32
+
+ + +
+
part+radius
+
t+r r8
+
b+l r12
+
t+b r8
+
noL r8
+
+ +
+
part+radius2
+
noB r8
+
noT r8
+
noR r8
+
t2+l6
+
+ + +
+
thick+radius
+
8px/r12
+
12/r16
+
3px/r32
+
t12
+
+ + +
+
large boxes
+
96x96
+
96x48
+
48x96
+
mixed96
+
+ +
+ + diff --git a/bin/unit_tests/assets/html/eepp-ui-border-rendering.webp b/bin/unit_tests/assets/html/eepp-ui-border-rendering.webp new file mode 100644 index 0000000000000000000000000000000000000000..d63bb804c161928e60bc0902a22a28e6c29024a5 GIT binary patch literal 7522 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 0; + bool hasRight = borderRight > 0; + bool hasBottom = borderBottom > 0; + bool hasLeft = borderLeft > 0; - if ( leftW ) { + 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 ) ); + + // Corner positions + Vector2f tlInner( pos.x + borderLeft, pos.y + borderTop ); + Vector2f tlOuter( pos.x, pos.y ); + Vector2f trInner( pos.x + size.getWidth() - borderRight, pos.y + borderTop ); + Vector2f trOuter( pos.x + size.getWidth(), pos.y ); + Vector2f brInner( pos.x + size.getWidth() - borderRight, + pos.y + size.getHeight() - borderBottom ); + Vector2f brOuter( pos.x + size.getWidth(), pos.y + size.getHeight() ); + Vector2f blInner( pos.x + borderLeft, pos.y + size.getHeight() - borderBottom ); + Vector2f blOuter( pos.x, pos.y + size.getHeight() ); + + // Helper: compute arc outer vertex at a given angle + auto arcOuterPos = []( const Vector2f& center, double rW, double rH, + double angleDeg ) -> Vector2f { + return Vector2f( center.x + rW * Math::cosAng( angleDeg ), + center.y + rH * Math::sinAng( angleDeg ) ); + }; + + // 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 ) ); + return basePos; + }; + + // Pre-compute first inner vertex of each border (used as bridge targets) + // Top border first inner (top-left corner) + 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 ) ); + } else { + topFirstInner = tlInner; + } + + // Right border first inner (top-right corner) + Vector2f rightFirstInner; + if ( trArcW > 0 && hasTop ) { + Vector2f trCenter( pos.x + size.getWidth() - trArcW, pos.y + trArcH ); + rightFirstInner = + arcInnerPos( trCenter, trArcW, trArcH, 315, borderRight, + Vector2f( pos.x + size.getWidth() - borderRight, pos.y + borderTop ) ); + } else { + rightFirstInner = trInner; + } + + // 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 ) ); + } else { + bottomFirstInner = brInner; + } + + // Left border first inner (bottom-left corner) + Vector2f leftFirstInner; + 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 ) ); + } else { + leftFirstInner = blInner; + } + + // Helper: insert degenerate triangle bridge between two disconnected border sections + auto addBridge = [&]( const Vector2f& fromOuter, const Vector2f& toInner, + const Color& bridgeColor ) { + vbo->addVertex( fromOuter ); + vbo->addColor( bridgeColor ); + vbo->addVertex( toInner ); + vbo->addColor( bridgeColor ); + vbo->addVertex( toInner ); + vbo->addColor( bridgeColor ); + }; + + Vector2f lastOuter; // last emitted outer vertex, used as bridge source + + // --- draw top border --- + if ( hasTop ) { + double leftW = tlArcW; + double rightW = trArcW; + double leftH = tlArcH; + double rightH = trArcH; + + if ( leftW && hasLeft ) { double endAngle = 270; double startAngle = 225; @@ -170,7 +276,7 @@ void Borders::createBorders( VertexBuffer* vbo, const Borders& borders, const Ve vbo->addColor( borders.top.color ); } - if ( rightW ) { + if ( rightW && hasRight ) { double startAngle = 270; double endAngle = 315; Vector2f basePos( pos.x + size.getWidth() - borderRight, pos.y + borderTop ); @@ -191,22 +297,33 @@ void Borders::createBorders( VertexBuffer* vbo, const Borders& borders, const Ve borderAddArc( vbo, tPos, rightW, rightH, startAngle, endAngle, borders.top.color, borderTop, basePos ); + + lastOuter = arcOuterPos( tPos, rightW, rightH, endAngle ); } else { vbo->addVertex( Vector2f( pos.x + size.getWidth() - borderRight, pos.y + borderTop ) ); vbo->addColor( borders.top.color ); vbo->addVertex( Vector2f( pos.x + size.getWidth(), pos.y ) ); vbo->addColor( borders.top.color ); + + lastOuter = trOuter; + } + + if ( !hasRight ) { + if ( hasBottom ) + addBridge( lastOuter, bottomFirstInner, borders.top.color ); + else if ( hasLeft ) + addBridge( lastOuter, leftFirstInner, borders.top.color ); } } - // draw right border - if ( borderRight ) { - double topW = eemin( halfWidth, eemax( 0.f, borders.radius.topRight.x ) ); - double bottomW = eemin( halfHeight, eemax( 0.f, borders.radius.bottomRight.x ) ); - double topH = eemin( halfWidth, eemax( 0.f, borders.radius.topRight.y ) ); - double bottomH = eemin( halfHeight, eemax( 0.f, borders.radius.bottomRight.y ) ); + // --- draw right border --- + if ( hasRight ) { + double topW = trArcW; + double bottomW = brArcW; + double topH = trArcH; + double bottomH = brArcH; - if ( topW ) { + if ( topW && hasTop ) { double startAngle = 315; double endAngle = 360; Vector2f basePos( pos.x + size.getWidth() - borderRight, pos.y + borderTop ); @@ -220,7 +337,7 @@ void Borders::createBorders( VertexBuffer* vbo, const Borders& borders, const Ve vbo->addColor( borders.right.color ); } - if ( bottomH ) { + if ( bottomH && hasBottom ) { double startAngle = 0; double endAngle = 45; Vector2f basePos( pos.x + size.getWidth() - borderRight, @@ -243,23 +360,29 @@ void Borders::createBorders( VertexBuffer* vbo, const Borders& borders, const Ve borderAddArc( vbo, tPos, bottomW, bottomH, startAngle, endAngle, borders.right.color, borderRight, basePos ); + lastOuter = arcOuterPos( tPos, bottomW, bottomH, endAngle ); } else { vbo->addVertex( Vector2f( pos.x + size.getWidth() - borderRight, pos.y + size.getHeight() - borderBottom ) ); vbo->addColor( borders.right.color ); vbo->addVertex( Vector2f( pos.x + size.getWidth(), pos.y + size.getHeight() ) ); vbo->addColor( borders.right.color ); + + lastOuter = brOuter; } + + if ( !hasBottom && hasLeft ) + addBridge( lastOuter, leftFirstInner, borders.right.color ); } - // draw bottom border - if ( borderBottom ) { - double leftW = eemin( halfWidth, eemax( 0.f, borders.radius.bottomLeft.x ) ); - double rightW = eemin( halfHeight, eemax( 0.f, borders.radius.bottomRight.x ) ); - double leftH = eemin( halfWidth, eemax( 0.f, borders.radius.bottomLeft.y ) ); - double rightH = eemin( halfHeight, eemax( 0.f, borders.radius.bottomRight.y ) ); + // --- draw bottom border --- + if ( hasBottom ) { + double leftW = blArcW; + double rightW = brArcW; + double leftH = blArcH; + double rightH = brArcH; - if ( rightW ) { + if ( rightW && hasRight ) { double startAngle = 45; double endAngle = 90; Vector2f basePos( pos.x + size.getWidth() - borderRight, @@ -277,7 +400,7 @@ void Borders::createBorders( VertexBuffer* vbo, const Borders& borders, const Ve vbo->addColor( borders.bottom.color ); } - if ( leftW ) { + if ( leftW && hasLeft ) { double startAngle = 90; double endAngle = 135; Vector2f basePos( pos.x + borderLeft, pos.y + size.getHeight() - borderBottom ); @@ -299,23 +422,30 @@ void Borders::createBorders( VertexBuffer* vbo, const Borders& borders, const Ve borderAddArc( vbo, tPos, leftW, leftH, startAngle, endAngle, borders.bottom.color, borderBottom, basePos ); + + lastOuter = arcOuterPos( tPos, leftW, leftH, endAngle ); } else { vbo->addVertex( Vector2f( pos.x + borderLeft, pos.y + size.getHeight() - borderBottom ) ); vbo->addColor( borders.bottom.color ); vbo->addVertex( Vector2f( pos.x, pos.y + size.getHeight() ) ); vbo->addColor( borders.bottom.color ); + + lastOuter = blOuter; } + + // After bottom, only left remains (already checked or skipped). + // Bottom and left are adjacent, no bridge needed. } - // draw left border - if ( borderLeft ) { - double topW = eemin( halfWidth, eemax( 0.f, borders.radius.topLeft.x ) ); - double bottomW = eemin( halfHeight, eemax( 0.f, borders.radius.bottomLeft.x ) ); - double topH = eemin( halfWidth, eemax( 0.f, borders.radius.topLeft.y ) ); - double bottomH = eemin( halfHeight, eemax( 0.f, borders.radius.bottomLeft.y ) ); + // --- draw left border --- + if ( hasLeft ) { + double topW = tlArcW; + double bottomW = blArcW; + double topH = tlArcH; + double bottomH = blArcH; - if ( bottomW ) { + if ( bottomW && hasBottom ) { double startAngle = 135; double endAngle = 180; Vector2f basePos( pos.x + borderLeft, pos.y + size.getHeight() - borderBottom ); @@ -331,7 +461,7 @@ void Borders::createBorders( VertexBuffer* vbo, const Borders& borders, const Ve vbo->addColor( borders.left.color ); } - if ( topW ) { + if ( topW && hasTop ) { double startAngle = 180; double endAngle = 225; Vector2f basePos( pos.x + borderLeft, pos.y + borderTop ); diff --git a/src/eepp/ui/css/stylesheetpropertiesparser.cpp b/src/eepp/ui/css/stylesheetpropertiesparser.cpp index 2d5f2610e..90d742770 100644 --- a/src/eepp/ui/css/stylesheetpropertiesparser.cpp +++ b/src/eepp/ui/css/stylesheetpropertiesparser.cpp @@ -147,13 +147,13 @@ void StyleSheetPropertiesParser::addProperty( std::string name, std::string valu StyleSheetSpecification::instance()->getShorthand( name )->parse( value ); for ( auto& property : properties ) - mProperties.emplace( std::make_pair( property.getId(), std::move( property ) ) ); + mProperties[property.getId()] = std::move( property ); } else { if ( String::startsWith( name, "--" ) ) { mVariables[String::hash( name )] = StyleSheetVariable( name, value ); } else { StyleSheetProperty property( name, value ); - mProperties.emplace( std::make_pair( property.getId(), std::move( property ) ) ); + mProperties[property.getId()] = std::move( property ); } } } diff --git a/src/eepp/ui/uistyle.cpp b/src/eepp/ui/uistyle.cpp index 02aa2eb97..52199b5fa 100644 --- a/src/eepp/ui/uistyle.cpp +++ b/src/eepp/ui/uistyle.cpp @@ -210,12 +210,11 @@ UnorderedSet& UIStyle::getStructurallyVolatileChildren() { } const CSS::StyleSheetProperty* UIStyle::getProperty( const CSS::PropertyId& id ) { - const CSS::StyleSheetProperty* prop = nullptr; - if ( mGlobalDefinition && ( prop = mGlobalDefinition->getProperty( (Uint32)id ) ) ) - return prop; - if ( mElementStyle ) - prop = mElementStyle->getPropertyById( id ); - return prop; + const auto* gProp = mGlobalDefinition ? mGlobalDefinition->getProperty( (Uint32)id ) : nullptr; + const auto* elProp = mElementStyle ? mElementStyle->getPropertyById( id ) : nullptr; + if ( elProp && gProp ) + return elProp->getSpecificity() > gProp->getSpecificity() ? elProp : gProp; + return elProp ? elProp : gProp; } bool UIStyle::hasProperty( const CSS::PropertyId& propertyId ) const { diff --git a/src/eepp/ui/uiwidget.cpp b/src/eepp/ui/uiwidget.cpp index 46c5c3960..a8719bdc6 100644 --- a/src/eepp/ui/uiwidget.cpp +++ b/src/eepp/ui/uiwidget.cpp @@ -1846,7 +1846,7 @@ bool UIWidget::applyProperty( const StyleSheetProperty& attribute ) { StyleSheetSelectorRule::SpecificityImportant ) ); } setLayoutWidthPolicy( SizePolicy::Fixed ); - setSize( eefloor( lengthFromValueAsDp( attribute ) ), getSize().getHeight() ); + setSize( eefloor( lengthFromValueAsDp( attribute ) ), mDpSize.getHeight() ); notifyLayoutAttrChange(); } break; @@ -1860,7 +1860,7 @@ bool UIWidget::applyProperty( const StyleSheetProperty& attribute ) { StyleSheetSelectorRule::SpecificityImportant ) ); } setLayoutHeightPolicy( SizePolicy::Fixed ); - setSize( getSize().getWidth(), eefloor( lengthFromValueAsDp( attribute ) ) ); + setSize( mDpSize.getWidth(), eefloor( lengthFromValueAsDp( attribute ) ) ); notifyLayoutAttrChange(); } break; @@ -2276,12 +2276,12 @@ void UIWidget::loadFromXmlNode( const pugi::xml_node& node ) { StyleSheetPropertiesParser propertiesParser; propertiesParser.parse( std::string_view{ ait->value() } ); if ( !propertiesParser.getProperties().empty() ) { - for ( auto& [_, property] : propertiesParser.getProperties() ) { - auto propertyImportant( property ); - propertyImportant.setImportant( true ); - if ( mStyle ) - mStyle->setStyleSheetProperty( propertyImportant ); - applyProperty( propertyImportant ); + for ( auto& [_, prop] : propertiesParser.getProperties() ) { + auto property( prop ); + property.setSpecificity( StyleSheetSelectorRule::SpecificityInline ); + if ( NULL != mStyle ) + mStyle->setStyleSheetProperty( property ); + applyProperty( property ); } } continue; diff --git a/src/tests/unit_tests/uihtml_tests.cpp b/src/tests/unit_tests/uihtml_tests.cpp index 568efd85e..ddc7e8f45 100644 --- a/src/tests/unit_tests/uihtml_tests.cpp +++ b/src/tests/unit_tests/uihtml_tests.cpp @@ -802,3 +802,37 @@ UTEST( UILayout, listStyleInheritanceFromUl ) { Engine::destroySingleton(); } + +UTEST( UIBorder, renderingVariations ) { + auto win = Engine::instance()->createWindow( + WindowSettings( 1200, 900, "Border Rendering Test", WindowStyle::Default, + WindowBackend::Default, 32, {}, 1, false, true ), + ContextSettings( false, 0, 0, GLv_default, true, false ) ); + FileSystem::changeWorkingDirectory( Sys::getProcessPath() ); + + FontTrueType* font = FontTrueType::New( "NotoSans-Regular" ); + font->loadFromFile( "../assets/fonts/NotoSans-Regular.ttf" ); + ASSERT_TRUE( font != nullptr && font->loaded() ); + FontFamily::loadFromRegular( font ); + + UI::UISceneNode* sceneNode = UI::UISceneNode::New(); + SceneManager::instance()->add( sceneNode ); + UI::UIThemeManager* themeManager = sceneNode->getUIThemeManager(); + themeManager->setDefaultFont( font ); + sceneNode->setURI( "file://" + Sys::getProcessPath() + "assets/html/" ); + std::string html; + FileSystem::fileGet( "assets/html/border_tests.html", html ); + sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( html ) ); + win->setClearColor( Color::White ); + + win->getInput()->update(); + SceneManager::instance()->update(); + + win->clear(); + SceneManager::instance()->draw(); + win->display(); + + compareImages( utest_state, utest_result, win, "eepp-ui-border-rendering", "html" ); + + Engine::destroySingleton(); +}