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 000000000..d63bb804c Binary files /dev/null and b/bin/unit_tests/assets/html/eepp-ui-border-rendering.webp differ diff --git a/src/eepp/ui/border.cpp b/src/eepp/ui/border.cpp index 13f73d948..60a4e58f6 100644 --- a/src/eepp/ui/border.cpp +++ b/src/eepp/ui/border.cpp @@ -149,14 +149,120 @@ void Borders::createBorders( VertexBuffer* vbo, const Borders& borders, const Ve borderRight = eemin( (int)( size.getWidth() * 0.5f ), (int)borders.right.width ); } - // draw top border - if ( borderTop ) { - double leftW = eemin( halfWidth, eemax( 0.f, borders.radius.topLeft.x ) ); - double rightW = eemin( halfHeight, eemax( 0.f, borders.radius.topRight.x ) ); - double leftH = eemin( halfWidth, eemax( 0.f, borders.radius.topLeft.y ) ); - double rightH = eemin( halfHeight, eemax( 0.f, borders.radius.topRight.y ) ); + bool hasTop = borderTop > 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(); +}