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();
+}