diff --git a/include/eepp/ui/css/propertydefinition.hpp b/include/eepp/ui/css/propertydefinition.hpp index e02c7a5c0..6cbff682e 100644 --- a/include/eepp/ui/css/propertydefinition.hpp +++ b/include/eepp/ui/css/propertydefinition.hpp @@ -231,6 +231,7 @@ enum class PropertyId : Uint32 { MenuWidthMode = String::hash( "menu-width-mode" ), ExpandText = String::hash( "expand-text" ), Colspan = String::hash( "colspan" ), + TableLayout = String::hash( "table-layout" ), Cellpadding = String::hash( "cellpadding" ), Cellspacing = String::hash( "cellspacing" ), Size = String::hash( "size" ), diff --git a/include/eepp/ui/uihtmltable.hpp b/include/eepp/ui/uihtmltable.hpp index cea33e7f4..1e4394120 100644 --- a/include/eepp/ui/uihtmltable.hpp +++ b/include/eepp/ui/uihtmltable.hpp @@ -13,12 +13,18 @@ class UIHTMLTableHead; class UIHTMLTableBody; class UIHTMLTableFooter; +enum class TableLayout { Auto, Fixed }; + class EE_API UIHTMLTable : public UILayout { public: static UIHTMLTable* New(); UIHTMLTable(); + void setTableLayout( TableLayout layout ); + + TableLayout getTableLayout() const; + virtual Uint32 getType() const; virtual bool isType( const Uint32& type ) const; @@ -43,6 +49,7 @@ class EE_API UIHTMLTable : public UILayout { mutable SmallVector mColMinWidths; mutable SmallVector mColMaxWidths; mutable SmallVector mColSpecifiedWidths; + TableLayout mTableLayout{ TableLayout::Auto }; mutable UIHTMLTableHead* mHead{ nullptr }; mutable UIHTMLTableBody* mBody{ nullptr }; mutable UIHTMLTableFooter* mFooter{ nullptr }; diff --git a/src/eepp/ui/css/stylesheetspecification.cpp b/src/eepp/ui/css/stylesheetspecification.cpp index e8248fe6e..c932d3405 100644 --- a/src/eepp/ui/css/stylesheetspecification.cpp +++ b/src/eepp/ui/css/stylesheetspecification.cpp @@ -412,6 +412,7 @@ void StyleSheetSpecification::registerDefaultProperties() { registerProperty( "focusable", "true" ).setType( PropertyType::Bool ); registerProperty( "expand-text", "false" ).setType( PropertyType::Bool ); registerProperty( "colspan", "1" ).setType( PropertyType::NumberInt ); + registerProperty( "table-layout", "auto" ).setType( PropertyType::String ); registerProperty( "cellpadding", "0" ).setType( PropertyType::NumberLength ); registerProperty( "cellspacing", "0" ).setType( PropertyType::NumberLength ); registerProperty( "size", "20" ).setType( PropertyType::NumberInt ); diff --git a/src/eepp/ui/uihtmltable.cpp b/src/eepp/ui/uihtmltable.cpp index 80daf9e0b..25fc4bbdf 100644 --- a/src/eepp/ui/uihtmltable.cpp +++ b/src/eepp/ui/uihtmltable.cpp @@ -1,9 +1,14 @@ #include "eepp/ui/uistyle.hpp" #include +#include #include namespace EE { namespace UI { +static inline Float sanitizeFloat( Float val ) { + return std::isfinite( val ) ? val : 0.f; +} + UIHTMLTable* UIHTMLTable::New() { return eeNew( UIHTMLTable, () ); } @@ -14,6 +19,18 @@ UIHTMLTable::UIHTMLTable() : UILayout( "table" ) { mHeightPolicy = SizePolicy::WrapContent; } +void UIHTMLTable::setTableLayout( TableLayout layout ) { + if ( layout != mTableLayout ) { + mTableLayout = layout; + invalidateIntrinsicSize(); + tryUpdateLayout(); + } +} + +TableLayout UIHTMLTable::getTableLayout() const { + return mTableLayout; +} + Uint32 UIHTMLTable::getType() const { return UI_TYPE_HTML_TABLE; } @@ -37,6 +54,16 @@ bool UIHTMLTable::applyProperty( const StyleSheetProperty& attribute ) { invalidateIntrinsicSize(); tryUpdateLayout(); return true; + case PropertyId::TableLayout: { + std::string val = attribute.asString(); + String::toLowerInPlace( val ); + if ( val == "fixed" ) { + setTableLayout( TableLayout::Fixed ); + } else if ( val == "auto" ) { + setTableLayout( TableLayout::Auto ); + } + return true; + } default: break; } @@ -81,7 +108,7 @@ void UIHTMLTable::computeIntrinsicWidths() const { UIWidget* widget = node->asType(); Float spec = 0.f; if ( widget->getLayoutWidthPolicy() == SizePolicy::Fixed ) - spec = widget->getPropertyWidth(); + spec = sanitizeFloat( widget->getPropertyWidth() ); for ( Node* child = node->getFirstChild(); child; child = child->getNextNode() ) spec = std::max( spec, self( self, child ) ); return spec; @@ -121,81 +148,38 @@ void UIHTMLTable::computeIntrinsicWidths() const { me->mColMaxWidths.assign( maxCols, 0.f ); me->mColSpecifiedWidths.assign( maxCols, 0.f ); // 0 = no explicit width - // PASS 1: Collect intrinsic + explicit widths (single colspan first) - for ( size_t r = 0; r < mRows.size(); ++r ) { - Uint32 start = mRowCellOffsets[r]; - Uint32 end = mRowCellOffsets[r + 1]; - Uint32 colIndex = 0; + if ( mTableLayout == TableLayout::Fixed ) { + if ( !mRows.empty() ) { + Uint32 start = mRowCellOffsets[0]; + Uint32 end = mRowCellOffsets[1]; + Uint32 colIndex = 0; - for ( Uint32 i = 0; i < end - start; ++i ) { - UIHTMLTableCell* cell = mCells[start + i]; - auto widthPolicy = cell->getLayoutWidthPolicy(); - cell->mWidthPolicy = SizePolicy::WrapContent; - Float cellMin = cell->getMinIntrinsicWidth(); - Float cellMax = cell->getMaxIntrinsicWidth(); - Float cellSpecified = - std::max( cell->getPropertyWidth(), - getRecursiveSpecifiedWidth( getRecursiveSpecifiedWidth, cell ) ); - cell->mWidthPolicy = widthPolicy; + // PASS 1: Single colspan first row + for ( Uint32 i = 0; i < end - start; ++i ) { + UIHTMLTableCell* cell = mCells[start + i]; + Float cellSpecified = sanitizeFloat( + std::max( cell->getPropertyWidth(), + getRecursiveSpecifiedWidth( getRecursiveSpecifiedWidth, cell ) ) ); + Uint32 colspan = cell->getColspan(); - Uint32 colspan = cell->getColspan(); - - if ( colspan == 1 && colIndex < maxCols ) { - mColMinWidths[colIndex] = std::max( mColMinWidths[colIndex], cellMin ); - mColMaxWidths[colIndex] = std::max( mColMaxWidths[colIndex], cellMax ); - if ( cellSpecified > 0.f ) { - mColSpecifiedWidths[colIndex] = - std::max( mColSpecifiedWidths[colIndex], cellSpecified ); + if ( colspan == 1 && colIndex < maxCols ) { + if ( cellSpecified > 0.f ) { + mColSpecifiedWidths[colIndex] = std::max( mColSpecifiedWidths[colIndex], cellSpecified ); + } } + colIndex += colspan; } - colIndex += colspan; - } - } - // PASS 2: Multi-colspan cells - distribute excess only - for ( size_t r = 0; r < mRows.size(); ++r ) { - Uint32 start = mRowCellOffsets[r]; - Uint32 end = mRowCellOffsets[r + 1]; - Uint32 colIndex = 0; + // PASS 2: Multi-colspan cells first row + colIndex = 0; + for ( Uint32 i = 0; i < end - start; ++i ) { + UIHTMLTableCell* cell = mCells[start + i]; + Float cellSpecified = sanitizeFloat( + std::max( cell->getPropertyWidth(), + getRecursiveSpecifiedWidth( getRecursiveSpecifiedWidth, cell ) ) ); + Uint32 colspan = cell->getColspan(); - for ( Uint32 i = 0; i < end - start; ++i ) { - UIHTMLTableCell* cell = mCells[start + i]; - auto widthPolicy = cell->getLayoutWidthPolicy(); - cell->mWidthPolicy = SizePolicy::WrapContent; - Float cellMin = cell->getMinIntrinsicWidth(); - Float cellMax = cell->getMaxIntrinsicWidth(); - Float cellSpecified = - std::max( cell->getPropertyWidth(), - getRecursiveSpecifiedWidth( getRecursiveSpecifiedWidth, cell ) ); - cell->mWidthPolicy = widthPolicy; - - Uint32 colspan = cell->getColspan(); - - if ( colspan > 1 ) { - // Min excess - Float curMin = 0.f; - for ( Uint32 j = 0; j < colspan && colIndex + j < maxCols; ++j ) - curMin += mColMinWidths[colIndex + j]; - Float extraMin = std::max( 0.f, cellMin - curMin ); - if ( extraMin > 0.f ) { - Float add = extraMin / colspan; - for ( Uint32 j = 0; j < colspan && colIndex + j < maxCols; ++j ) - mColMinWidths[colIndex + j] += add; - } - - // Max excess - Float curMax = 0.f; - for ( Uint32 j = 0; j < colspan && colIndex + j < maxCols; ++j ) - curMax += mColMaxWidths[colIndex + j]; - Float extraMax = std::max( 0.f, cellMax - curMax ); - if ( extraMax > 0.f ) { - Float add = extraMax / colspan; - for ( Uint32 j = 0; j < colspan && colIndex + j < maxCols; ++j ) - mColMaxWidths[colIndex + j] += add; - } - - // Specified width excess (simple even distribution for now) - if ( cellSpecified > 0.f ) { + if ( colspan > 1 && cellSpecified > 0.f ) { Float curSpec = 0.f; for ( Uint32 j = 0; j < colspan && colIndex + j < maxCols; ++j ) curSpec += mColSpecifiedWidths[colIndex + j]; @@ -207,15 +191,106 @@ void UIHTMLTable::computeIntrinsicWidths() const { std::max( mColSpecifiedWidths[colIndex + j], add ); } } + colIndex += colspan; + } + } + } else { + // PASS 1: Collect intrinsic + explicit widths (single colspan first) + for ( size_t r = 0; r < mRows.size(); ++r ) { + Uint32 start = mRowCellOffsets[r]; + Uint32 end = mRowCellOffsets[r + 1]; + Uint32 colIndex = 0; + + for ( Uint32 i = 0; i < end - start; ++i ) { + UIHTMLTableCell* cell = mCells[start + i]; + auto widthPolicy = cell->getLayoutWidthPolicy(); + cell->mWidthPolicy = SizePolicy::WrapContent; + Float cellMin = sanitizeFloat( cell->getMinIntrinsicWidth() ); + Float cellMax = sanitizeFloat( cell->getMaxIntrinsicWidth() ); + Float cellSpecified = sanitizeFloat( + std::max( cell->getPropertyWidth(), + getRecursiveSpecifiedWidth( getRecursiveSpecifiedWidth, cell ) ) ); + cell->mWidthPolicy = widthPolicy; + + Uint32 colspan = cell->getColspan(); + + if ( colspan == 1 && colIndex < maxCols ) { + mColMinWidths[colIndex] = std::max( mColMinWidths[colIndex], cellMin ); + mColMaxWidths[colIndex] = std::max( mColMaxWidths[colIndex], cellMax ); + if ( cellSpecified > 0.f ) { + mColSpecifiedWidths[colIndex] = + std::max( mColSpecifiedWidths[colIndex], cellSpecified ); + } + } + colIndex += colspan; + } + } + + // PASS 2: Multi-colspan cells - distribute excess only + for ( size_t r = 0; r < mRows.size(); ++r ) { + Uint32 start = mRowCellOffsets[r]; + Uint32 end = mRowCellOffsets[r + 1]; + Uint32 colIndex = 0; + + for ( Uint32 i = 0; i < end - start; ++i ) { + UIHTMLTableCell* cell = mCells[start + i]; + auto widthPolicy = cell->getLayoutWidthPolicy(); + cell->mWidthPolicy = SizePolicy::WrapContent; + Float cellMin = sanitizeFloat( cell->getMinIntrinsicWidth() ); + Float cellMax = sanitizeFloat( cell->getMaxIntrinsicWidth() ); + Float cellSpecified = sanitizeFloat( + std::max( cell->getPropertyWidth(), + getRecursiveSpecifiedWidth( getRecursiveSpecifiedWidth, cell ) ) ); + cell->mWidthPolicy = widthPolicy; + + Uint32 colspan = cell->getColspan(); + + if ( colspan > 1 ) { + // Min excess + Float curMin = 0.f; + for ( Uint32 j = 0; j < colspan && colIndex + j < maxCols; ++j ) + curMin += mColMinWidths[colIndex + j]; + Float extraMin = std::max( 0.f, cellMin - curMin ); + if ( extraMin > 0.f ) { + Float add = extraMin / colspan; + for ( Uint32 j = 0; j < colspan && colIndex + j < maxCols; ++j ) + mColMinWidths[colIndex + j] += add; + } + + // Max excess + Float curMax = 0.f; + for ( Uint32 j = 0; j < colspan && colIndex + j < maxCols; ++j ) + curMax += mColMaxWidths[colIndex + j]; + Float extraMax = std::max( 0.f, cellMax - curMax ); + if ( extraMax > 0.f ) { + Float add = extraMax / colspan; + for ( Uint32 j = 0; j < colspan && colIndex + j < maxCols; ++j ) + mColMaxWidths[colIndex + j] += add; + } + + // Specified width excess (simple even distribution for now) + if ( cellSpecified > 0.f ) { + Float curSpec = 0.f; + for ( Uint32 j = 0; j < colspan && colIndex + j < maxCols; ++j ) + curSpec += mColSpecifiedWidths[colIndex + j]; + Float extraSpec = std::max( 0.f, cellSpecified - curSpec ); + if ( extraSpec > 0.f ) { + Float add = extraSpec / colspan; + for ( Uint32 j = 0; j < colspan && colIndex + j < maxCols; ++j ) + mColSpecifiedWidths[colIndex + j] = + std::max( mColSpecifiedWidths[colIndex + j], add ); + } + } + } + colIndex += colspan; } - colIndex += colspan; } } Float totalMin = 0.f, totalMax = 0.f; for ( size_t i = 0; i < maxCols; ++i ) { - mColMinWidths[i] = std::max( mColMinWidths[i], mColSpecifiedWidths[i] ); - mColMaxWidths[i] = std::max( mColMaxWidths[i], mColSpecifiedWidths[i] ); + mColMinWidths[i] = sanitizeFloat( std::max( mColMinWidths[i], mColSpecifiedWidths[i] ) ); + mColMaxWidths[i] = sanitizeFloat( std::max( mColMaxWidths[i], mColSpecifiedWidths[i] ) ); totalMin += mColMinWidths[i]; totalMax += mColMaxWidths[i]; } @@ -261,8 +336,8 @@ void UIHTMLTable::updateLayout() { mColWidths.assign( maxCols, 0.f ); Float paddingH = mPaddingPx.Left + mPaddingPx.Right; Float containerWidth = getPixelsSize().getWidth(); - Float availableWidth = - std::max( 0.f, containerWidth - paddingH - ( maxCols + 1 ) * mCellspacing ); + Float availableWidth = sanitizeFloat( + std::max( 0.f, containerWidth - paddingH - ( maxCols + 1 ) * mCellspacing ) ); if ( availableWidth <= 0.f || maxCols == 0 ) { mPacking = false; @@ -271,14 +346,41 @@ void UIHTMLTable::updateLayout() { Float totalMin = 0.f; Float totalMax = 0.f; // Make sure this is uncommented for ( size_t i = 0; i < maxCols; ++i ) { - totalMin += mColMinWidths[i]; - totalMax += mColMaxWidths[i]; // Accumulate max widths + totalMin += sanitizeFloat( mColMinWidths[i] ); + totalMax += sanitizeFloat( mColMaxWidths[i] ); // Accumulate max widths } Float tableUsedWidth = availableWidth; // always try to fill the container // Assign column widths - if ( tableUsedWidth <= totalMin + 0.001f ) { + if ( mTableLayout == TableLayout::Fixed ) { + Float sumOfSpecifiedWidths = 0.f; + size_t unspecifiedCount = 0; + for ( size_t i = 0; i < maxCols; ++i ) { + if ( mColSpecifiedWidths[i] > 0.f ) { + sumOfSpecifiedWidths += mColSpecifiedWidths[i]; + mColWidths[i] = mColSpecifiedWidths[i]; + } else { + unspecifiedCount++; + } + } + + Float remainingSpace = std::max( 0.f, availableWidth - sumOfSpecifiedWidths ); + + if ( unspecifiedCount > 0 ) { + Float share = remainingSpace / static_cast( unspecifiedCount ); + for ( size_t i = 0; i < maxCols; ++i ) { + if ( mColSpecifiedWidths[i] <= 0.f ) { + mColWidths[i] = share; + } + } + } else if ( remainingSpace > 0.f && sumOfSpecifiedWidths > 0.f ) { + for ( size_t i = 0; i < maxCols; ++i ) { + Float scale = mColSpecifiedWidths[i] / sumOfSpecifiedWidths; + mColWidths[i] += remainingSpace * scale; + } + } + } else if ( tableUsedWidth <= totalMin + 0.001f ) { // 1. Too narrow → scale down proportionally to min widths Float scale = totalMin > 0.001f ? ( tableUsedWidth / totalMin ) : 0.f; for ( size_t i = 0; i < maxCols; ++i ) @@ -367,6 +469,9 @@ void UIHTMLTable::updateLayout() { mColWidths[i] = w; } + for ( float& w : mColWidths ) + w = sanitizeFloat( w ); + Float headHeight = 0; Float bodyHeight = 0; Float footerHeight = 0; diff --git a/src/tests/unit_tests/uihtml_tests.cpp b/src/tests/unit_tests/uihtml_tests.cpp index ddd89bfb9..272866454 100644 --- a/src/tests/unit_tests/uihtml_tests.cpp +++ b/src/tests/unit_tests/uihtml_tests.cpp @@ -309,3 +309,51 @@ UTEST( HTMLTextArea, rowsColsAttribute ) { Engine::destroySingleton(); } + +UTEST( UIHTMLTable, tableLayoutFixed ) { + Engine::instance()->createWindow( WindowSettings( 1024, 650, "HTML Tables Test", + WindowStyle::Default, WindowBackend::Default, + 32, {}, 1, false, true ) ); + 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->loadLayoutFromString( + R"( + + + + + + + + + + +
C1C2C3
C4 (Should be ignored)C5C6
)" ); + + sceneNode->updateDirtyLayouts(); + + auto c1 = sceneNode->getRoot()->find( "c1" ); + auto c2 = sceneNode->getRoot()->find( "c2" ); + auto c3 = sceneNode->getRoot()->find( "c3" ); + + ASSERT_TRUE( c1 != nullptr ); + ASSERT_TRUE( c2 != nullptr ); + ASSERT_TRUE( c3 != nullptr ); + + // Total width is 600px. C1=100, C2=200, C3 takes remaining 300px. + EXPECT_NEAR( c1->getPixelsSize().getWidth(), 100.f, 1.f ); + EXPECT_NEAR( c2->getPixelsSize().getWidth(), 200.f, 1.f ); + EXPECT_NEAR( c3->getPixelsSize().getWidth(), 300.f, 1.f ); + + Engine::destroySingleton(); +}