From ad4e4285d6739a4e1446cfe9b53cccbc203673b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=ADn=20Lucas=20Golini?= Date: Tue, 2 Jun 2026 12:29:37 -0300 Subject: [PATCH] Grid fixes. --- bin/unit_tests/assets/html/grid_test.html | 63 ++++++++++++ include/eepp/ui/gridlayouter.hpp | 1 + src/eepp/ui/gridlayouter.cpp | 80 ++++++++++------ src/eepp/ui/uihtmlwidget.cpp | 25 +++++ src/tests/unit_tests/uihtml_grid_test.cpp | 111 ++++++++++++++++++++++ 5 files changed, 250 insertions(+), 30 deletions(-) create mode 100644 bin/unit_tests/assets/html/grid_test.html 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,