Grid fixes.

This commit is contained in:
Martín Lucas Golini
2026-06-02 12:29:37 -03:00
parent 2f8e631e88
commit ad4e4285d6
5 changed files with 250 additions and 30 deletions

View File

@@ -0,0 +1,63 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<style id="css-output">
* {
box-sizing: border-box;
}
.wrapper {
max-width: 940px;
margin: 0 auto;
}
.wrapper > div {
border: 2px solid rgb(233 171 88);
border-radius: 5px;
background-color: rgb(233 171 88 / 50%);
padding: 1em;
color: #d9480f;
}
.wrapper {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 10px;
grid-auto-rows: minmax(100px, auto);
}
.one {
grid-column: 1 / 3;
grid-row: 1;
}
.two {
grid-column: 2 / 4;
grid-row: 1 / 3;
}
.three {
grid-column: 1;
grid-row: 2 / 5;
}
.four {
grid-column: 3;
grid-row: 3;
}
.five {
grid-column: 2;
grid-row: 4;
}
.six {
grid-column: 3;
grid-row: 4;
}
</style>
</head>
<body>
<div class="wrapper">
<div class="one">One</div>
<div class="two">Two</div>
<div class="three">Three</div>
<div class="four">Four</div>
<div class="five">Five</div>
<div class="six">Six</div>
</div>
</body>
</html>

View File

@@ -157,6 +157,7 @@ class EE_API GridLayouter : public UILayouter {
Float mContainerBaseline{ 0.f };
int mMaxColumn{ 0 };
int mMaxRow{ 0 };
CSSGridAutoFlow mAutoFlow{ CSSGridAutoFlow::Row };
bool mAutoFlowDense{ false };
bool mColAutoRepeatIsFit{ false };
bool mRowAutoRepeatIsFit{ false };

View File

@@ -255,6 +255,30 @@ static bool isValidTrackBreadthType( const GridTrackBreadth& b ) {
b.type == GridTrackBreadthType::Percentage || b.type == GridTrackBreadthType::Flex;
}
static GridTrackSize parseAutoTrackSize( const std::string& value ) {
GridTrackSize size;
std::string trimmed = String::trim( value );
String::toLowerInPlace( trimmed );
if ( trimmed.empty() || trimmed == "auto" )
return size;
GridTrackList list = GridTrackParser::parseTrackList( trimmed );
if ( list.valid && !list.none && list.tracks.size() == 1 )
return list.tracks[0].size;
GridTrackBreadth b = parseTrackBreadth( trimmed );
if ( b.valid ) {
size.min = b;
size.max = b;
}
return size;
}
static bool isFlexibleTrack( const GridTrack& track ) {
return track.definition.min.type == GridTrackBreadthType::Flex ||
track.definition.max.type == GridTrackBreadthType::Flex;
}
// ── Public API ──
GridTrackList GridTrackParser::parseTrackList( const std::string& value ) {
@@ -453,24 +477,11 @@ void GridLayouter::readContainerStyle() {
mRowGap = mContainer->lengthFromValue(
grid->getRowGap(), CSS::PropertyRelativeTarget::ContainingBlockHeight, 0.f );
mTemplateAreas = GridAreasParser::parseAreas( grid->getGridTemplateAreas() );
mAutoFlow = grid->getGridAutoFlow();
mAutoFlowDense = grid->getGridAutoFlowDense();
mAutoColumnSize = GridTrackSize{};
mAutoRowSize = GridTrackSize{};
std::string autoColsStr = grid->getGridAutoColumns();
std::string autoRowsStr = grid->getGridAutoRows();
String::toLowerInPlace( autoColsStr );
String::toLowerInPlace( autoRowsStr );
if ( autoColsStr != "auto" && !autoColsStr.empty() ) {
GridTrackBreadth b = parseTrackBreadth( autoColsStr );
mAutoColumnSize.min = b;
mAutoColumnSize.max = b;
}
if ( autoRowsStr != "auto" && !autoRowsStr.empty() ) {
GridTrackBreadth b = parseTrackBreadth( autoRowsStr );
mAutoRowSize.min = b;
mAutoRowSize.max = b;
}
mAutoColumnSize = parseAutoTrackSize( grid->getGridAutoColumns() );
mAutoRowSize = parseAutoTrackSize( grid->getGridAutoRows() );
// Parse and expand column tracks (including auto-repeat)
GridTrackList colList = GridTrackParser::parseTrackList( grid->getGridTemplateColumns() );
@@ -651,6 +662,12 @@ void GridLayouter::sizeTracksForAxis( bool isColumns ) {
Float spannedSize = 0.f;
int spanStart = start - 1;
int spanEnd = std::min( end - 1, static_cast<int>( tracks.size() ) );
bool allSpannedTracksAreFlexible = true;
for ( int i = spanStart; i < spanEnd; ++i )
allSpannedTracksAreFlexible =
allSpannedTracksAreFlexible && isFlexibleTrack( tracks[i] );
if ( allSpannedTracksAreFlexible )
continue;
for ( int i = spanStart; i < spanEnd; ++i )
spannedSize += tracks[i].baseSize;
spannedSize += static_cast<Float>( span - 1 ) * ( isColumns ? mColumnGap : mRowGap );
@@ -746,6 +763,7 @@ void GridLayouter::preSizeItemsForRowSizing() {
if ( ce > cs + 1 )
cellW += static_cast<Float>( ce - cs - 1 ) * mColumnGap;
auto oldWidthPolicy = item.widget->getLayoutWidthPolicy();
item.widget->setLayoutWidthPolicy( SizePolicy::Fixed );
item.widget->setPixelsSize( cellW, item.widget->getPixelsSize().getHeight() );
@@ -759,6 +777,7 @@ void GridLayouter::preSizeItemsForRowSizing() {
childHtml->setLayoutHeightPolicy( oldHP );
}
}
item.widget->setLayoutWidthPolicy( oldWidthPolicy );
}
}
@@ -876,9 +895,9 @@ void GridLayouter::applyLayout() {
Float cellH = rowLines[re] - rowLines[rs];
// Add internal gaps for multi-span items
if ( ce > cs + 1 )
cellW += static_cast<Float>( ce - cs - 1 ) * mColumnGap;
cellW += static_cast<Float>( ce - cs - 1 ) * colGapX;
if ( re > rs + 1 )
cellH += static_cast<Float>( re - rs - 1 ) * mRowGap;
cellH += static_cast<Float>( re - rs - 1 ) * rowGapY;
// Determine effective alignment
CSSJustifySelf js = item.justifySelf;
@@ -947,6 +966,11 @@ void GridLayouter::updateLayout() {
UIHTMLWidget* grid =
mContainer->isType( UI_TYPE_HTML_WIDGET ) ? mContainer->asType<UIHTMLWidget>() : nullptr;
if ( grid && grid->getLayoutWidthPolicy() == SizePolicy::MatchParent ) {
Float matchWidth = grid->getMatchParentWidth();
if ( matchWidth > 0.f && eeabs( matchWidth - grid->getPixelsSize().getWidth() ) > 0.01f )
grid->setInternalPixelsWidth( matchWidth );
}
readContainerStyle();
collectGridItems();
resolveDefinitePlacements();
@@ -959,13 +983,11 @@ void GridLayouter::updateLayout() {
bool wPercentUnresolved = false;
if ( grid && grid->getUIStyle() ) {
const StyleSheetProperty* hprop = grid->getUIStyle()->getProperty( PropertyId::Height );
hPercentUnresolved =
hprop && StyleSheetLength::isPercentage( hprop->value() ) &&
grid->getPixelsSize().getHeight() <= 0.f;
hPercentUnresolved = hprop && StyleSheetLength::isPercentage( hprop->value() ) &&
grid->getPixelsSize().getHeight() <= 0.f;
const StyleSheetProperty* wprop = grid->getUIStyle()->getProperty( PropertyId::Width );
wPercentUnresolved =
wprop && StyleSheetLength::isPercentage( wprop->value() ) &&
grid->getPixelsSize().getWidth() <= 0.f;
wPercentUnresolved = wprop && StyleSheetLength::isPercentage( wprop->value() ) &&
grid->getPixelsSize().getWidth() <= 0.f;
}
for ( int sizingPass = 0; sizingPass < 2 && needResize; ++sizingPass ) {
needResize = false;
@@ -980,9 +1002,8 @@ void GridLayouter::updateLayout() {
contentH += row.baseSize;
if ( mRows.size() > 1 )
contentH += static_cast<Float>( mRows.size() - 1 ) * mRowGap;
contentH +=
mContainer->getPixelsContentOffset().Top +
mContainer->getPixelsContentOffset().Bottom;
contentH += mContainer->getPixelsContentOffset().Top +
mContainer->getPixelsContentOffset().Bottom;
if ( contentH > 0.f ) {
mContainer->setInternalPixelsHeight( contentH );
needResize = true;
@@ -990,8 +1011,7 @@ void GridLayouter::updateLayout() {
}
if ( wPercentUnresolved ) {
const StyleSheetProperty* wprop =
grid->getUIStyle()->getProperty( PropertyId::Width );
const StyleSheetProperty* wprop = grid->getUIStyle()->getProperty( PropertyId::Width );
std::string v = wprop->value();
v.pop_back();
Float pct = 0.f;
@@ -1347,7 +1367,7 @@ void GridLayouter::autoPlaceItems() {
int cursorCol = 1;
int cursorRow = 1;
bool isRowFlow = true;
bool isRowFlow = mAutoFlow == CSSGridAutoFlow::Row;
auto occupyRange = [&]( int r, int c, int w, int h ) {
for ( int dr = 0; dr < h; ++dr )

View File

@@ -696,6 +696,17 @@ bool UIHTMLWidget::applyProperty( const StyleSheetProperty& attribute ) {
if ( !checkPropertyDefinition( attribute ) )
return false;
auto applyGridLineShorthand = []( const std::string& value, auto setStart, auto setEnd ) {
size_t slash = value.find( '/' );
if ( slash == std::string::npos ) {
setStart( String::trim( value ) );
setEnd( "auto" );
} else {
setStart( String::trim( value.substr( 0, slash ) ) );
setEnd( String::trim( value.substr( slash + 1 ) ) );
}
};
switch ( attribute.getPropertyDefinition()->getPropertyId() ) {
case PropertyId::Display: {
setDisplay( CSSDisplayHelper::fromString( attribute.asString() ) );
@@ -845,6 +856,20 @@ bool UIHTMLWidget::applyProperty( const StyleSheetProperty& attribute ) {
setGridColumnEnd( attribute.asString() );
return true;
}
case PropertyId::GridRow: {
applyGridLineShorthand(
attribute.asString(),
[this]( const std::string& value ) { setGridRowStart( value ); },
[this]( const std::string& value ) { setGridRowEnd( value ); } );
return true;
}
case PropertyId::GridColumn: {
applyGridLineShorthand(
attribute.asString(),
[this]( const std::string& value ) { setGridColumnStart( value ); },
[this]( const std::string& value ) { setGridColumnEnd( value ); } );
return true;
}
case PropertyId::GridArea: {
setGridArea( attribute.asString() );
return true;

View File

@@ -233,6 +233,44 @@ UTEST( GridAutoPlacement, rowFlowFillsColumnsThenRows ) {
Engine::destroySingleton();
}
UTEST( GridAutoPlacement, columnFlowFillsRowsThenColumns ) {
Engine::instance()->createWindow( WindowSettings( 1024, 650, "Grid Test", WindowStyle::Default,
WindowBackend::Default, 32, {}, 1, false,
true ),
ContextSettings( false, 0, 0, GLv_default, true, false ) );
init_grid_test();
UISceneNode* sceneNode = SceneManager::instance()->getUISceneNode();
UIHTMLWidget* grid = UIHTMLWidget::New();
grid->setParent( sceneNode->getRoot() );
grid->setDisplay( CSSDisplay::Grid );
grid->setGridTemplateRows( "50px 50px" );
grid->setGridAutoFlow( CSSGridAutoFlow::Column );
grid->setPixelsSize( 500, 200 );
grid->setLayoutSizePolicy( SizePolicy::Fixed, SizePolicy::Fixed );
UIHTMLWidget* i1 = UIHTMLWidget::New();
i1->setParent( grid );
UIHTMLWidget* i2 = UIHTMLWidget::New();
i2->setParent( grid );
UIHTMLWidget* i3 = UIHTMLWidget::New();
i3->setParent( grid );
sceneNode->updateDirtyLayouts();
auto* layouter = static_cast<GridLayouter*>( grid->getLayouter() );
const auto& items = layouter->getItems();
ASSERT_EQ( items.size(), 3u );
EXPECT_EQ( items[0].resolvedRowStart, 1 );
EXPECT_EQ( items[0].resolvedColumnStart, 1 );
EXPECT_EQ( items[1].resolvedRowStart, 2 );
EXPECT_EQ( items[1].resolvedColumnStart, 1 );
EXPECT_EQ( items[2].resolvedRowStart, 1 );
EXPECT_EQ( items[2].resolvedColumnStart, 2 );
Engine::destroySingleton();
}
UTEST( GridAutoPlacement, definiteLeavesHoleForSparse ) {
Engine::instance()->createWindow( WindowSettings( 1024, 650, "Grid Test", WindowStyle::Default,
WindowBackend::Default, 32, {}, 1, false,
@@ -1717,6 +1755,79 @@ UTEST( GridContainer, downloadGridLayout ) {
Engine::destroySingleton();
}
UTEST( GridContainer, gridTestFixturePlacesSpanningItems ) {
Engine::instance()->createWindow( WindowSettings( 1024, 650, "UIHTML Grid Fixture Test",
WindowStyle::Default, WindowBackend::Default,
32, {}, 1, false, true ),
ContextSettings() );
init_grid_test();
UISceneNode* sceneNode = SceneManager::instance()->getUISceneNode();
sceneNode->setURI( "file://" + Sys::getProcessPath() + "assets/html/" );
std::string htmlContent;
ASSERT_TRUE( FileSystem::fileGet( "assets/html/grid_test.html", htmlContent ) );
sceneNode->loadLayoutFromString( EE::UI::Tools::HTMLFormatter::HTMLtoXML( htmlContent ) );
sceneNode->update( Seconds( 1 ) );
sceneNode->updateDirtyLayouts();
auto* wrapperNode = sceneNode->getRoot()->findByClass( "wrapper" );
ASSERT_TRUE( nullptr != wrapperNode );
ASSERT_TRUE( wrapperNode->isWidget() );
auto* one = sceneNode->getRoot()->findByClass( "one" );
auto* two = sceneNode->getRoot()->findByClass( "two" );
auto* three = sceneNode->getRoot()->findByClass( "three" );
auto* four = sceneNode->getRoot()->findByClass( "four" );
auto* five = sceneNode->getRoot()->findByClass( "five" );
auto* six = sceneNode->getRoot()->findByClass( "six" );
ASSERT_TRUE( nullptr != one );
ASSERT_TRUE( nullptr != two );
ASSERT_TRUE( nullptr != three );
ASSERT_TRUE( nullptr != four );
ASSERT_TRUE( nullptr != five );
ASSERT_TRUE( nullptr != six );
ASSERT_TRUE( one->isWidget() );
ASSERT_TRUE( two->isWidget() );
ASSERT_TRUE( three->isWidget() );
ASSERT_TRUE( four->isWidget() );
ASSERT_TRUE( five->isWidget() );
ASSERT_TRUE( six->isWidget() );
auto* oneWidget = one->asType<UIWidget>();
auto* twoWidget = two->asType<UIWidget>();
auto* threeWidget = three->asType<UIWidget>();
auto* fourWidget = four->asType<UIWidget>();
auto* fiveWidget = five->asType<UIWidget>();
auto* sixWidget = six->asType<UIWidget>();
Float wrapperW = wrapperNode->asType<UIWidget>()->getPixelsSize().getWidth();
EXPECT_GT( wrapperW, 900.f );
EXPECT_LT( wrapperW, 950.f );
EXPECT_GT( oneWidget->getPixelsSize().getWidth(), 600.f );
EXPECT_GT( twoWidget->getPixelsSize().getWidth(), 600.f );
EXPECT_GT( threeWidget->getPixelsSize().getWidth(), 290.f );
EXPECT_GT( fourWidget->getPixelsSize().getWidth(), 290.f );
EXPECT_GT( fiveWidget->getPixelsSize().getWidth(), 290.f );
EXPECT_GT( sixWidget->getPixelsSize().getWidth(), 290.f );
EXPECT_GT( oneWidget->getPixelsSize().getHeight(), 95.f );
EXPECT_GT( twoWidget->getPixelsSize().getHeight(), 200.f );
EXPECT_GT( threeWidget->getPixelsSize().getHeight(), 300.f );
EXPECT_GT( fourWidget->getPixelsSize().getHeight(), 95.f );
EXPECT_GT( fiveWidget->getPixelsSize().getHeight(), 95.f );
EXPECT_GT( sixWidget->getPixelsSize().getHeight(), 95.f );
EXPECT_NEAR( oneWidget->getPixelsPosition().y, twoWidget->getPixelsPosition().y, 1.f );
EXPECT_GT( twoWidget->getPixelsPosition().x, oneWidget->getPixelsPosition().x + 300.f );
EXPECT_GT( threeWidget->getPixelsPosition().y, oneWidget->getPixelsPosition().y + 95.f );
EXPECT_GT( fourWidget->getPixelsPosition().y, twoWidget->getPixelsPosition().y + 200.f );
EXPECT_NEAR( fiveWidget->getPixelsPosition().y, sixWidget->getPixelsPosition().y, 1.f );
EXPECT_GT( sixWidget->getPixelsPosition().x, fiveWidget->getPixelsPosition().x + 300.f );
Engine::destroySingleton();
}
UTEST( GridContainer, newsblurReducedGrid ) {
Engine::instance()->createWindow( WindowSettings( 1024, 650, "UIHTML Grid NewsBlur Test",
WindowStyle::Default, WindowBackend::Default,