mirror of
https://github.com/SpartanJ/eepp.git
synced 2026-08-10 08:41:24 +03:00
Optimized layouting of UIHTMLTable.
Fixed in HTMLFormatter not handling correctly spaces after `<br/>`.
This commit is contained in:
@@ -6,6 +6,9 @@
|
||||
|
||||
namespace EE { namespace UI {
|
||||
|
||||
class UIHTMLTableRow;
|
||||
class UIHTMLTableCell;
|
||||
|
||||
class EE_API UIHTMLTable : public UILayout {
|
||||
public:
|
||||
static UIHTMLTable* New();
|
||||
@@ -20,6 +23,11 @@ class EE_API UIHTMLTable : public UILayout {
|
||||
|
||||
protected:
|
||||
virtual Uint32 onMessage( const NodeMessage* Msg );
|
||||
|
||||
std::vector<UIHTMLTableRow*> mRows;
|
||||
std::vector<Float> mColWidths;
|
||||
std::vector<UIHTMLTableCell*> mCells;
|
||||
std::vector<Uint32> mRowCellOffsets;
|
||||
};
|
||||
|
||||
class EE_API UIHTMLTableCell : public UIRichText {
|
||||
|
||||
@@ -175,22 +175,24 @@ String HTMLFormatter::collapseXmlWhitespace( const String& text, const pugi::xml
|
||||
}
|
||||
}
|
||||
|
||||
// Step 2: Determine if the left boundary is a block element.
|
||||
// Step 2: Determine if the left boundary is a block element or a forced line break (<br/>).
|
||||
// We use getLogicalPrev, and if the previous node is just empty space
|
||||
// (lacks significant text), we keep looking further back.
|
||||
pugi::xml_node prev = getLogicalPrev( node );
|
||||
while ( prev && prev.type() == pugi::node_pcdata && !hasSignificantText( prev ) ) {
|
||||
prev = getLogicalPrev( prev );
|
||||
}
|
||||
bool prevInline = isInlineNode( prev );
|
||||
// A node is a valid inline neighbor only if it is an inline node AND not a <br/>
|
||||
// (because <br/> strips adjacent whitespace).
|
||||
bool prevInline = isInlineNode( prev ) && !String::iequals( prev.name(), "br" );
|
||||
|
||||
// Step 3: Determine if the right boundary is a block element.
|
||||
// Step 3: Determine if the right boundary is a block element or a forced line break.
|
||||
// We use getLogicalNext, skipping over any non-significant text nodes.
|
||||
pugi::xml_node next = getLogicalNext( node );
|
||||
while ( next && next.type() == pugi::node_pcdata && !hasSignificantText( next ) ) {
|
||||
next = getLogicalNext( next );
|
||||
}
|
||||
bool nextInline = isInlineNode( next );
|
||||
bool nextInline = isInlineNode( next ) && !String::iequals( next.name(), "br" );
|
||||
|
||||
// Step 4: Trim leading and trailing spaces if they adjoin a block boundary.
|
||||
if ( !prevInline && !res.empty() && res[0] == ' ' )
|
||||
|
||||
@@ -25,17 +25,15 @@ void UIHTMLTable::updateLayout() {
|
||||
return;
|
||||
mPacking = true;
|
||||
|
||||
// TODO: Optimize this horrendous implementation (fix the heap-allocation crazyness)
|
||||
UIHTMLTableHead* head = nullptr;
|
||||
UIHTMLTableBody* body = nullptr;
|
||||
UIHTMLTableFooter* footer = nullptr;
|
||||
|
||||
std::vector<UIHTMLTableRow*> rows;
|
||||
std::function<void( Node* )> collectRows = [&]( Node* node ) {
|
||||
Node* child = node->getFirstChild();
|
||||
while ( child ) {
|
||||
mRows.clear();
|
||||
auto collectRows = [&]( auto self, Node* node ) -> void {
|
||||
for ( Node* child = node->getFirstChild(); child; child = child->getNextNode() ) {
|
||||
if ( child->getType() == UI_TYPE_HTML_TABLE_ROW ) {
|
||||
rows.push_back( child->asType<UIHTMLTableRow>() );
|
||||
mRows.push_back( child->asType<UIHTMLTableRow>() );
|
||||
} else if ( child->getType() != UI_TYPE_HTML_TABLE ) {
|
||||
if ( child->getType() == UI_TYPE_HTML_TABLE_HEAD )
|
||||
head = child->asType<UIHTMLTableHead>();
|
||||
@@ -44,30 +42,31 @@ void UIHTMLTable::updateLayout() {
|
||||
else if ( child->getType() == UI_TYPE_HTML_TABLE_FOOTER )
|
||||
footer = child->asType<UIHTMLTableFooter>();
|
||||
|
||||
collectRows( child );
|
||||
self( self, child );
|
||||
}
|
||||
child = child->getNextNode();
|
||||
}
|
||||
};
|
||||
collectRows( this );
|
||||
collectRows( collectRows, this );
|
||||
|
||||
if ( rows.empty() ) {
|
||||
if ( mRows.empty() ) {
|
||||
mPacking = false;
|
||||
return;
|
||||
}
|
||||
|
||||
std::vector<std::vector<UIHTMLTableCell*>> grid;
|
||||
mCells.clear();
|
||||
mRowCellOffsets.clear();
|
||||
mRowCellOffsets.push_back( 0 );
|
||||
size_t maxCols = 0;
|
||||
for ( auto* row : rows ) {
|
||||
std::vector<UIHTMLTableCell*> cells;
|
||||
Node* child = row->getFirstChild();
|
||||
while ( child ) {
|
||||
if ( child->getType() == UI_TYPE_HTML_TABLE_CELL )
|
||||
cells.push_back( child->asType<UIHTMLTableCell>() );
|
||||
child = child->getNextNode();
|
||||
for ( auto* row : mRows ) {
|
||||
size_t cellCount = 0;
|
||||
for ( Node* child = row->getFirstChild(); child; child = child->getNextNode() ) {
|
||||
if ( child->getType() == UI_TYPE_HTML_TABLE_CELL ) {
|
||||
mCells.push_back( child->asType<UIHTMLTableCell>() );
|
||||
cellCount++;
|
||||
}
|
||||
}
|
||||
grid.push_back( cells );
|
||||
maxCols = std::max( maxCols, cells.size() );
|
||||
mRowCellOffsets.push_back( (Uint32)mCells.size() );
|
||||
maxCols = std::max( maxCols, cellCount );
|
||||
}
|
||||
|
||||
if ( maxCols == 0 ) {
|
||||
@@ -75,31 +74,29 @@ void UIHTMLTable::updateLayout() {
|
||||
return;
|
||||
}
|
||||
|
||||
std::vector<Float> colWidths( maxCols, 0.f );
|
||||
mColWidths.assign( maxCols, 0.f );
|
||||
|
||||
// Get natural width for each column (without wrapping)
|
||||
for ( const auto& rowCells : grid ) {
|
||||
for ( size_t i = 0; i < rowCells.size(); ++i ) {
|
||||
UIHTMLTableCell* cell = rowCells[i];
|
||||
for ( size_t r = 0; r < mRows.size(); ++r ) {
|
||||
Uint32 start = mRowCellOffsets[r];
|
||||
Uint32 end = mRowCellOffsets[r + 1];
|
||||
for ( Uint32 i = 0; i < end - start; ++i ) {
|
||||
UIHTMLTableCell* cell = mCells[start + i];
|
||||
cell->setLayoutWidthPolicy( SizePolicy::WrapContent );
|
||||
cell->updateLayout();
|
||||
colWidths[i] = std::max( colWidths[i], cell->getPixelsSize().getWidth() );
|
||||
mColWidths[i] = std::max( mColWidths[i], cell->getPixelsSize().getWidth() );
|
||||
}
|
||||
}
|
||||
|
||||
Float availableWidth = getPixelsSize().getWidth() - mPaddingPx.Left - mPaddingPx.Right;
|
||||
Float totalUnwrappedWidth = 0;
|
||||
for ( Float w : colWidths )
|
||||
for ( Float w : mColWidths )
|
||||
totalUnwrappedWidth += w;
|
||||
|
||||
if ( totalUnwrappedWidth > availableWidth && totalUnwrappedWidth > 0 ) {
|
||||
if ( totalUnwrappedWidth > 0 ) {
|
||||
Float scale = availableWidth / totalUnwrappedWidth;
|
||||
for ( size_t i = 0; i < maxCols; ++i )
|
||||
colWidths[i] *= scale;
|
||||
} else if ( totalUnwrappedWidth < availableWidth && maxCols > 0 && totalUnwrappedWidth > 0 ) {
|
||||
Float scale = availableWidth / totalUnwrappedWidth;
|
||||
for ( size_t i = 0; i < maxCols; ++i )
|
||||
colWidths[i] *= scale;
|
||||
mColWidths[i] *= scale;
|
||||
}
|
||||
|
||||
Float headHeight = 0;
|
||||
@@ -107,35 +104,37 @@ void UIHTMLTable::updateLayout() {
|
||||
Float footerHeight = 0;
|
||||
|
||||
// Apply layout and calculate heights
|
||||
size_t rowCount = grid.size();
|
||||
size_t rowCount = mRows.size();
|
||||
for ( size_t r = 0; r < rowCount; ++r ) {
|
||||
Float rowHeight = 0;
|
||||
size_t columnCount = grid[r].size();
|
||||
for ( size_t c = 0; c < columnCount; ++c ) {
|
||||
UIHTMLTableCell* cell = grid[r][c];
|
||||
Uint32 start = mRowCellOffsets[r];
|
||||
Uint32 end = mRowCellOffsets[r + 1];
|
||||
Uint32 columnCount = end - start;
|
||||
for ( Uint32 c = 0; c < columnCount; ++c ) {
|
||||
UIHTMLTableCell* cell = mCells[start + c];
|
||||
cell->setLayoutWidthPolicy( SizePolicy::Fixed );
|
||||
cell->setPixelsSize( colWidths[c], cell->getPixelsSize().getHeight() );
|
||||
cell->setPixelsSize( mColWidths[c], cell->getPixelsSize().getHeight() );
|
||||
cell->updateLayout();
|
||||
rowHeight = std::max( rowHeight, cell->getPixelsSize().getHeight() );
|
||||
}
|
||||
|
||||
// Position cells inside the row and equalize height
|
||||
Float currentX = 0;
|
||||
for ( size_t c = 0; c < columnCount; ++c ) {
|
||||
UIHTMLTableCell* cell = grid[r][c];
|
||||
for ( Uint32 c = 0; c < columnCount; ++c ) {
|
||||
UIHTMLTableCell* cell = mCells[start + c];
|
||||
cell->setPixelsPosition( currentX, 0 );
|
||||
cell->setPixelsSize( cell->getPixelsSize().getWidth(), rowHeight );
|
||||
currentX += colWidths[c];
|
||||
currentX += mColWidths[c];
|
||||
}
|
||||
|
||||
// Set row height and width
|
||||
UIHTMLTableRow* row = rows[r];
|
||||
UIHTMLTableRow* row = mRows[r];
|
||||
row->setPixelsSize( availableWidth, rowHeight );
|
||||
|
||||
if ( r == 0 ) {
|
||||
headHeight = rowHeight;
|
||||
} else if ( r == rowCount - 1 && columnCount &&
|
||||
grid[r][0]->getParent()->isType( UI_TYPE_HTML_TABLE_FOOTER ) ) {
|
||||
mCells[start]->getParent()->isType( UI_TYPE_HTML_TABLE_FOOTER ) ) {
|
||||
footerHeight = rowHeight;
|
||||
} else {
|
||||
bodyHeight += rowHeight;
|
||||
@@ -143,8 +142,8 @@ void UIHTMLTable::updateLayout() {
|
||||
}
|
||||
|
||||
// Position rows vertically
|
||||
// We also need to ensure that the containers (thead, tbody, etc.) are positioned at 0,0
|
||||
// and have the correct size, so the absolute positioning of the rows works as expected.
|
||||
// We also need to ensure that the containers (thead, tbody, etc.) are positioned correctly and
|
||||
// have the correct size, so the absolute positioning of the rows works as expected.
|
||||
if ( head ) {
|
||||
head->setPixelsPosition( 0, 0 );
|
||||
head->setPixelsSize( { getPixelsSize().x, headHeight } );
|
||||
@@ -161,15 +160,18 @@ void UIHTMLTable::updateLayout() {
|
||||
}
|
||||
|
||||
Float currentY = mPaddingPx.Top - headHeight;
|
||||
for ( auto* row : rows ) {
|
||||
for ( size_t r = 0; r < rowCount; ++r ) {
|
||||
UIHTMLTableRow* row = mRows[r];
|
||||
row->setPixelsPosition( mPaddingPx.Left, currentY );
|
||||
currentY += row->getPixelsSize().getHeight();
|
||||
}
|
||||
if ( head && !rows.empty() )
|
||||
rows[0]->setPixelsPosition( mPaddingPx.Left, 0 );
|
||||
|
||||
if ( footer && !rows.empty() )
|
||||
rows[rowCount - 1]->setPixelsPosition( mPaddingPx.Left, 0 );
|
||||
// Reset positions if they are inside specialized containers
|
||||
if ( head && !mRows.empty() )
|
||||
mRows[0]->setPixelsPosition( mPaddingPx.Left, 0 );
|
||||
|
||||
if ( footer && !mRows.empty() )
|
||||
mRows[rowCount - 1]->setPixelsPosition( mPaddingPx.Left, 0 );
|
||||
|
||||
if ( mWidthPolicy == SizePolicy::MatchParent )
|
||||
setInternalPixelsWidth( getMatchParentWidth() );
|
||||
|
||||
@@ -836,3 +836,55 @@ UTEST( UIHTMLTable, basicLayout ) {
|
||||
eeDelete( sceneNode );
|
||||
Engine::destroySingleton();
|
||||
}
|
||||
|
||||
UTEST( UIRichText, WhitespaceCollapseBRTest ) {
|
||||
Engine::instance()->createWindow( WindowSettings( 800, 600, "RichText 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->loaded() );
|
||||
FontFamily::loadFromRegular( font );
|
||||
|
||||
UI::UISceneNode* sceneNode = UI::UISceneNode::New();
|
||||
UI::UIThemeManager* themeManager = sceneNode->getUIThemeManager();
|
||||
themeManager->setDefaultFont( font );
|
||||
|
||||
String xml = R"xml(
|
||||
<h1 align="center" id="rt">
|
||||
<img src="icon" /><br/>
|
||||
ecode
|
||||
</h1>
|
||||
)xml";
|
||||
|
||||
sceneNode->loadLayoutFromString( xml );
|
||||
|
||||
UI::UIRichText* rt = sceneNode->find<UI::UIRichText>( "rt" );
|
||||
ASSERT_TRUE( rt != nullptr );
|
||||
|
||||
sceneNode->update( Time::Zero );
|
||||
|
||||
// The "ecode" text span should NOT have a leading space.
|
||||
bool foundEcodeWithLeadingSpace = false;
|
||||
auto checkSpansRecursive = [&]( Node* n, auto&& checkSpansRecursiveRef ) -> void {
|
||||
if ( !n ) return;
|
||||
if ( n->isWidget() && n->isType( UI_TYPE_TEXTSPAN ) ) {
|
||||
UI::UITextSpan* span = static_cast<UI::UITextSpan*>( n );
|
||||
if ( span->getText().size() > 0 && span->getText()[0] == ' ' &&
|
||||
span->getText().find( "ecode" ) != String::InvalidPos ) {
|
||||
foundEcodeWithLeadingSpace = true;
|
||||
}
|
||||
}
|
||||
for ( Node* child = n->getFirstChild(); child; child = child->getNextNode() ) {
|
||||
checkSpansRecursiveRef( child, checkSpansRecursiveRef );
|
||||
}
|
||||
};
|
||||
checkSpansRecursive( rt, checkSpansRecursive );
|
||||
EXPECT_FALSE( foundEcodeWithLeadingSpace );
|
||||
|
||||
eeDelete( sceneNode );
|
||||
Engine::destroySingleton();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user