diff --git a/bin/unit_tests/assets/html/grid_test.html b/bin/unit_tests/assets/html/grid_test.html
new file mode 100644
index 000000000..cfb6059a7
--- /dev/null
+++ b/bin/unit_tests/assets/html/grid_test.html
@@ -0,0 +1,63 @@
+
+
+
+
+
+
+
+
+
+
One
+
Two
+
Three
+
Four
+
Five
+
Six
+
+
+
diff --git a/include/eepp/ui/gridlayouter.hpp b/include/eepp/ui/gridlayouter.hpp
index 0a4c9f719..f256ef723 100644
--- a/include/eepp/ui/gridlayouter.hpp
+++ b/include/eepp/ui/gridlayouter.hpp
@@ -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 };
diff --git a/src/eepp/ui/gridlayouter.cpp b/src/eepp/ui/gridlayouter.cpp
index b8a635580..47d4534b3 100644
--- a/src/eepp/ui/gridlayouter.cpp
+++ b/src/eepp/ui/gridlayouter.cpp
@@ -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( 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( span - 1 ) * ( isColumns ? mColumnGap : mRowGap );
@@ -746,6 +763,7 @@ void GridLayouter::preSizeItemsForRowSizing() {
if ( ce > cs + 1 )
cellW += static_cast( 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( ce - cs - 1 ) * mColumnGap;
+ cellW += static_cast( ce - cs - 1 ) * colGapX;
if ( re > rs + 1 )
- cellH += static_cast( re - rs - 1 ) * mRowGap;
+ cellH += static_cast( 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() : 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( 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 )
diff --git a/src/eepp/ui/uihtmlwidget.cpp b/src/eepp/ui/uihtmlwidget.cpp
index b0ffbe85c..04f3437be 100644
--- a/src/eepp/ui/uihtmlwidget.cpp
+++ b/src/eepp/ui/uihtmlwidget.cpp
@@ -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;
diff --git a/src/tests/unit_tests/uihtml_grid_test.cpp b/src/tests/unit_tests/uihtml_grid_test.cpp
index 0878aca77..c42bc6f61 100644
--- a/src/tests/unit_tests/uihtml_grid_test.cpp
+++ b/src/tests/unit_tests/uihtml_grid_test.cpp
@@ -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( 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();
+ auto* twoWidget = two->asType();
+ auto* threeWidget = three->asType();
+ auto* fourWidget = four->asType();
+ auto* fiveWidget = five->asType();
+ auto* sixWidget = six->asType();
+
+ Float wrapperW = wrapperNode->asType()->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,