mirror of
https://github.com/SpartanJ/eepp.git
synced 2026-10-25 23:57:53 +03:00
Improve table-layout algorithms and added fixed table layout support.
This commit is contained in:
@@ -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" ),
|
||||
|
||||
@@ -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<Float> mColMinWidths;
|
||||
mutable SmallVector<Float> mColMaxWidths;
|
||||
mutable SmallVector<Float> mColSpecifiedWidths;
|
||||
TableLayout mTableLayout{ TableLayout::Auto };
|
||||
mutable UIHTMLTableHead* mHead{ nullptr };
|
||||
mutable UIHTMLTableBody* mBody{ nullptr };
|
||||
mutable UIHTMLTableFooter* mFooter{ nullptr };
|
||||
|
||||
@@ -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 );
|
||||
|
||||
+183
-78
@@ -1,9 +1,14 @@
|
||||
#include "eepp/ui/uistyle.hpp"
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <eepp/ui/uihtmltable.hpp>
|
||||
|
||||
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<UIWidget>();
|
||||
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<Float>( 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;
|
||||
|
||||
@@ -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"(<table style="width: 600px; table-layout: fixed;">
|
||||
<tr>
|
||||
<td id="c1" style="width: 100px;">C1</td>
|
||||
<td id="c2" style="width: 200px;">C2</td>
|
||||
<td id="c3">C3</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="width: 500px;">C4 (Should be ignored)</td>
|
||||
<td>C5</td>
|
||||
<td>C6</td>
|
||||
</tr>
|
||||
</table>)" );
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user