Add basic HTML tables support, this allows us to display tables in Markdown files.

This commit is contained in:
Martín Lucas Golini
2026-03-08 21:51:30 -03:00
parent 24ec4eed87
commit d45ffbfe23
7 changed files with 568 additions and 6 deletions

View File

@@ -5,14 +5,14 @@ All build commands must be executed from the **root project directory**. Follow
## Step 1: Update Makefiles (Conditional)
If you have **added, renamed, or deleted** any source files, you must regenerate the makefiles before compiling.
* **Tool:** Use `premake5` if installed; otherwise, fallback to `premake4` (the parameters are identical).
* **Tool:** Use `premake4` if installed; otherwise, fallback to `premake5` (the parameters are identical).
* **Linker Flag (`--with-mold-linker`):** This flag is conditional. If the `mold` linker is installed on the system, you **must** include it to speed up linking. If `mold` is not installed, omit the flag.
**Command (if `mold` is installed):**
`premake5 --disable-static-build --with-mold-linker --with-debug-symbols --address-sanitizer gmake`
`premake4 --disable-static-build --with-mold-linker --with-debug-symbols --address-sanitizer gmake`
**Command (if `mold` is NOT installed):**
`premake5 --disable-static-build --with-debug-symbols --address-sanitizer gmake`
`premake4 --disable-static-build --with-debug-symbols --address-sanitizer gmake`
*(If no files were added/removed, you may skip Step 1).*

View File

@@ -111,6 +111,12 @@ enum UINodeType {
UI_TYPE_TEXTSPAN,
UI_TYPE_RICHTEXT,
UI_TYPE_MARKDOWNVIEW,
UI_TYPE_HTML_TABLE,
UI_TYPE_HTML_TABLE_HEAD,
UI_TYPE_HTML_TABLE_BODY,
UI_TYPE_HTML_TABLE_FOOTER,
UI_TYPE_HTML_TABLE_ROW,
UI_TYPE_HTML_TABLE_CELL,
UI_TYPE_MODULES = 10000,
UI_TYPE_TERMINAL = 10001,
UI_TYPE_USER = 200000,

View File

@@ -0,0 +1,82 @@
#ifndef EE_UI_UIHTMLTABLE_HPP
#define EE_UI_UIHTMLTABLE_HPP
#include <eepp/ui/uilayout.hpp>
#include <eepp/ui/uirichtext.hpp>
namespace EE { namespace UI {
class EE_API UIHTMLTable : public UILayout {
public:
static UIHTMLTable* New();
UIHTMLTable();
virtual Uint32 getType() const;
virtual bool isType( const Uint32& type ) const;
virtual void updateLayout();
protected:
virtual Uint32 onMessage( const NodeMessage* Msg );
};
class EE_API UIHTMLTableCell : public UIRichText {
public:
static UIHTMLTableCell* New( const std::string& tag );
explicit UIHTMLTableCell( const std::string& tag );
virtual Uint32 getType() const;
virtual bool isType( const Uint32& type ) const;
};
class EE_API UIHTMLTableRow : public UIWidget {
public:
static UIHTMLTableRow* New();
UIHTMLTableRow();
virtual Uint32 getType() const;
virtual bool isType( const Uint32& type ) const;
};
class EE_API UIHTMLTableHead : public UIWidget {
public:
static UIHTMLTableHead* New();
explicit UIHTMLTableHead();
virtual Uint32 getType() const;
virtual bool isType( const Uint32& type ) const;
};
class EE_API UIHTMLTableFooter : public UIWidget {
public:
static UIHTMLTableFooter* New();
explicit UIHTMLTableFooter();
virtual Uint32 getType() const;
virtual bool isType( const Uint32& type ) const;
};
class EE_API UIHTMLTableBody : public UIWidget {
public:
static UIHTMLTableBody* New();
explicit UIHTMLTableBody();
virtual Uint32 getType() const;
virtual bool isType( const Uint32& type ) const;
};
}} // namespace EE::UI
#endif

View File

@@ -7,6 +7,25 @@
namespace EE { namespace UI { namespace Tools {
// In HTML, whitespace processing depends heavily on whether elements are block-level
// or inline-level. The HTML specification states that sequences of whitespace
// (spaces, tabs, newlines) inside inline formatting contexts are collapsed into a
// single space, but leading and trailing spaces are removed entirely if they adjoin
// a block boundary (e.g. at the start or end of a `<p>` or `<div>`).
//
// For example:
// <p>
// <a href="...">
// <img />
// </a>
// </p>
// In this snippet, the spaces and newlines between `<p>` and `<a>` are completely
// dropped because they touch the block boundary of `<p>`. The spaces between `<a>` and
// `<img>` are inside an inline context, but because `<img/>` and `<a>` are inline, they
// might normally produce a space, except leading/trailing rules can apply depending on
// significant text content. To properly emulate HTML's visual rendering, we must
// identify whether a node acts as an "inline" element.
bool HTMLFormatter::isInlineNode( const pugi::xml_node& node ) {
if ( !node )
return false;
@@ -14,6 +33,8 @@ bool HTMLFormatter::isInlineNode( const pugi::xml_node& node ) {
return true;
if ( node.type() != pugi::node_element )
return false;
// Compare element tags against known HTML inline elements and our internal equivalents.
std::string_view name( node.name() );
return String::iequals( name, "a" ) || String::iequals( name, "span" ) ||
String::iequals( name, "textspan" ) || String::iequals( name, "b" ) ||
@@ -24,9 +45,22 @@ bool HTMLFormatter::isInlineNode( const pugi::xml_node& node ) {
String::iequals( name, "mark" );
}
// "Significant text" in the context of HTML whitespace collapsing means any text
// that is not entirely composed of whitespace characters, or elements that have a
// visual inline presence like images (<img/>) or line breaks (<br/>).
// Empty inline elements (e.g. `<span></span>`) or those containing only whitespace
// are often ignored when evaluating boundaries for whitespace trimming.
//
// This function allows us to peer inside nodes or text blocks to see if they actually
// contain anything that visually anchors a whitespace sequence. If a node lacks
// significant text, the whitespace logic can skip over it to find the true logical
// boundary.
bool HTMLFormatter::hasSignificantText( const pugi::xml_node& node ) {
if ( !node )
return false;
// For plain text, check if there's any non-whitespace character.
if ( node.type() == pugi::node_pcdata ) {
std::string_view v( node.value() );
for ( char c : v ) {
@@ -35,6 +69,9 @@ bool HTMLFormatter::hasSignificantText( const pugi::xml_node& node ) {
}
return false;
}
// For inline elements, certain tags are inherently significant (img, br).
// Otherwise, we recursively check their children.
if ( isInlineNode( node ) ) {
std::string_view name( node.name() );
if ( String::iequals( name, "img" ) || String::iequals( name, "br" ) )
@@ -45,18 +82,35 @@ bool HTMLFormatter::hasSignificantText( const pugi::xml_node& node ) {
}
return false;
}
return true; // Block nodes are significant boundaries
// Block nodes inherently form a significant boundary. We don't look inside them
// because a block node interrupts the inline formatting context entirely.
return true;
}
// In HTML, elements can be nested arbitrarily, meaning the "previous" inline node
// visually preceding a text block might not be its direct sibling in the DOM tree.
// For instance, in `<span><b>text</b></span> <img/>`, the space is technically a sibling
// of `<span>`, but logically it follows `<b>text</b>`.
//
// `getLogicalPrev` traverses the DOM tree backward, diving into the rightmost children
// of previous siblings, or walking up to the parent, as long as the traversed nodes
// remain within the inline formatting context. This effectively finds the closest
// visual element to the left of the current node.
pugi::xml_node HTMLFormatter::getLogicalPrev( const pugi::xml_node& node ) {
pugi::xml_node p = node;
while ( p ) {
// If there is a previous sibling, we move to it and then drill down
// to its last (rightmost) inline child, simulating visual left-to-right flow.
if ( p.previous_sibling() ) {
p = p.previous_sibling();
while ( p.last_child() && isInlineNode( p ) )
p = p.last_child();
return p;
}
// If there are no previous siblings, we move up to the parent.
// If the parent is a block element, we've hit a block boundary and stop.
p = p.parent();
if ( !isInlineNode( p ) )
break;
@@ -64,15 +118,22 @@ pugi::xml_node HTMLFormatter::getLogicalPrev( const pugi::xml_node& node ) {
return pugi::xml_node();
}
// `getLogicalNext` is the counterpart to `getLogicalPrev`. It traverses the DOM tree
// forward to find the closest visual element to the right of the current node.
// It drills into the leftmost children of next siblings, or walks up the parent tree
// to continue the forward search, bounded by block elements.
pugi::xml_node HTMLFormatter::getLogicalNext( const pugi::xml_node& node ) {
pugi::xml_node p = node;
while ( p ) {
// Move to the next sibling and drill down to its first (leftmost) inline child.
if ( p.next_sibling() ) {
p = p.next_sibling();
while ( p.first_child() && isInlineNode( p ) )
p = p.first_child();
return p;
}
// Move up to the parent, stopping at block boundaries.
p = p.parent();
if ( !isInlineNode( p ) )
break;
@@ -80,10 +141,27 @@ pugi::xml_node HTMLFormatter::getLogicalNext( const pugi::xml_node& node ) {
return pugi::xml_node();
}
// This function implements HTML-compliant whitespace collapsing for a given text node.
//
// HTML rules dictate that:
// 1. Any contiguous sequence of whitespace characters (spaces, tabs, newlines)
// is collapsed into a single space character (' ').
// 2. If this text node logically adjoins a block element (e.g. it is the first or last
// thing inside a `<div>`), the leading or trailing space is completely removed.
// 3. To accurately determine boundaries, we must skip over "empty" text nodes or
// elements that lack significant visual content.
//
// This function first collapses all whitespace into single spaces. Then it looks both
// logically backward and forward using `getLogicalPrev` and `getLogicalNext`. If it
// determines that there is no valid inline node on a given side (meaning it has hit
// a block boundary), it strips the space on that side.
String HTMLFormatter::collapseXmlWhitespace( const String& text, const pugi::xml_node& node ) {
String res;
res.reserve( text.size() );
bool inSpace = false;
// Step 1: Collapse all contiguous whitespace characters into a single space.
for ( size_t i = 0; i < text.size(); ++i ) {
if ( text[i] == ' ' || text[i] == '\t' || text[i] == '\n' || text[i] == '\r' ||
text[i] == '\v' ) {
@@ -97,18 +175,24 @@ String HTMLFormatter::collapseXmlWhitespace( const String& text, const pugi::xml
}
}
// Step 2: Determine if the left boundary is a block element.
// 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 );
// Step 3: Determine if the right boundary is a block element.
// 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 );
// Step 4: Trim leading and trailing spaces if they adjoin a block boundary.
if ( !prevInline && !res.empty() && res[0] == ' ' )
res = res.substr( 1 );

283
src/eepp/ui/uihtmltable.cpp Normal file
View File

@@ -0,0 +1,283 @@
#include <algorithm>
#include <eepp/ui/uihtmltable.hpp>
namespace EE { namespace UI {
UIHTMLTable* UIHTMLTable::New() {
return eeNew( UIHTMLTable, () );
}
UIHTMLTable::UIHTMLTable() : UILayout( "table" ) {
mWidthPolicy = SizePolicy::MatchParent;
mHeightPolicy = SizePolicy::WrapContent;
}
Uint32 UIHTMLTable::getType() const {
return UI_TYPE_HTML_TABLE;
}
bool UIHTMLTable::isType( const Uint32& type ) const {
return UIHTMLTable::getType() == type || UILayout::isType( type );
}
void UIHTMLTable::updateLayout() {
if ( mPacking || !mVisible )
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 ) {
if ( child->getType() == UI_TYPE_HTML_TABLE_ROW ) {
rows.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>();
else if ( child->getType() == UI_TYPE_HTML_TABLE_BODY )
body = child->asType<UIHTMLTableBody>();
else if ( child->getType() == UI_TYPE_HTML_TABLE_FOOTER )
footer = child->asType<UIHTMLTableFooter>();
collectRows( child );
}
child = child->getNextNode();
}
};
collectRows( this );
if ( rows.empty() ) {
mPacking = false;
return;
}
std::vector<std::vector<UIHTMLTableCell*>> grid;
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();
}
grid.push_back( cells );
maxCols = std::max( maxCols, cells.size() );
}
if ( maxCols == 0 ) {
mPacking = false;
return;
}
std::vector<Float> colWidths( 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];
cell->setLayoutWidthPolicy( SizePolicy::WrapContent );
cell->updateLayout();
colWidths[i] = std::max( colWidths[i], cell->getPixelsSize().getWidth() );
}
}
Float availableWidth = getPixelsSize().getWidth() - mPaddingPx.Left - mPaddingPx.Right;
Float totalUnwrappedWidth = 0;
for ( Float w : colWidths )
totalUnwrappedWidth += w;
if ( totalUnwrappedWidth > availableWidth && 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;
}
Float headHeight = 0;
Float bodyHeight = 0;
Float footerHeight = 0;
// Apply layout and calculate heights
size_t rowCount = grid.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];
cell->setLayoutWidthPolicy( SizePolicy::Fixed );
cell->setPixelsSize( colWidths[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];
cell->setPixelsPosition( currentX, 0 );
cell->setPixelsSize( cell->getPixelsSize().getWidth(), rowHeight );
currentX += colWidths[c];
}
// Set row height and width
UIHTMLTableRow* row = rows[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 ) ) {
footerHeight = rowHeight;
} else {
bodyHeight += rowHeight;
}
}
// 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.
if ( head ) {
head->setPixelsPosition( 0, 0 );
head->setPixelsSize( { getPixelsSize().x, headHeight } );
}
if ( body ) {
body->setPixelsPosition( 0, headHeight );
body->setPixelsSize( { getPixelsSize().x, bodyHeight } );
}
if ( footer ) {
footer->setPixelsPosition( 0, headHeight + bodyHeight );
footer->setPixelsSize( { getPixelsSize().x, footerHeight } );
}
Float currentY = mPaddingPx.Top - headHeight;
for ( auto* row : rows ) {
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 );
if ( mWidthPolicy == SizePolicy::MatchParent )
setInternalPixelsWidth( getMatchParentWidth() );
if ( mHeightPolicy == SizePolicy::WrapContent ) {
setInternalPixelsHeight( headHeight + bodyHeight + footerHeight + mPaddingPx.Bottom );
} else if ( mHeightPolicy == SizePolicy::MatchParent ) {
setInternalPixelsHeight( getMatchParentHeight() );
}
mPacking = false;
mDirtyLayout = false;
}
Uint32 UIHTMLTable::onMessage( const NodeMessage* Msg ) {
switch ( Msg->getMsg() ) {
case NodeMessage::LayoutAttributeChange: {
tryUpdateLayout();
return 1;
}
}
return 0;
}
UIHTMLTableRow* UIHTMLTableRow::New() {
return eeNew( UIHTMLTableRow, () );
}
UIHTMLTableRow::UIHTMLTableRow() : UIWidget( "tr" ) {
mWidthPolicy = SizePolicy::MatchParent;
mHeightPolicy = SizePolicy::WrapContent;
}
Uint32 UIHTMLTableRow::getType() const {
return UI_TYPE_HTML_TABLE_ROW;
}
bool UIHTMLTableRow::isType( const Uint32& type ) const {
return UIHTMLTableRow::getType() == type || UIWidget::isType( type );
}
UIHTMLTableCell* UIHTMLTableCell::New( const std::string& tag ) {
return eeNew( UIHTMLTableCell, ( tag ) );
}
UIHTMLTableCell::UIHTMLTableCell( const std::string& tag ) : UIRichText( tag ) {
mWidthPolicy = SizePolicy::WrapContent;
mHeightPolicy = SizePolicy::WrapContent;
}
Uint32 UIHTMLTableCell::getType() const {
return UI_TYPE_HTML_TABLE_CELL;
}
bool UIHTMLTableCell::isType( const Uint32& type ) const {
return UIHTMLTableCell::getType() == type || UIRichText::isType( type );
}
UIHTMLTableHead* UIHTMLTableHead::New() {
return eeNew( UIHTMLTableHead, () );
}
UIHTMLTableHead::UIHTMLTableHead() : UIWidget( "thead" ) {
mWidthPolicy = SizePolicy::MatchParent;
mHeightPolicy = SizePolicy::WrapContent;
}
Uint32 UIHTMLTableHead::getType() const {
return UI_TYPE_HTML_TABLE_HEAD;
}
bool UIHTMLTableHead::isType( const Uint32& type ) const {
return UIHTMLTableHead::getType() == type || UIWidget::isType( type );
}
UIHTMLTableBody* UIHTMLTableBody::New() {
return eeNew( UIHTMLTableBody, () );
}
UIHTMLTableBody::UIHTMLTableBody() : UIWidget( "tbody" ) {
mWidthPolicy = SizePolicy::MatchParent;
mHeightPolicy = SizePolicy::WrapContent;
}
Uint32 UIHTMLTableBody::getType() const {
return UI_TYPE_HTML_TABLE_BODY;
}
bool UIHTMLTableBody::isType( const Uint32& type ) const {
return UIHTMLTableBody::getType() == type || UIWidget::isType( type );
}
UIHTMLTableFooter* UIHTMLTableFooter::New() {
return eeNew( UIHTMLTableFooter, () );
}
UIHTMLTableFooter::UIHTMLTableFooter() : UIWidget( "tfoot" ) {
mWidthPolicy = SizePolicy::MatchParent;
mHeightPolicy = SizePolicy::WrapContent;
}
Uint32 UIHTMLTableFooter::getType() const {
return UI_TYPE_HTML_TABLE_FOOTER;
}
bool UIHTMLTableFooter::isType( const Uint32& type ) const {
return UIHTMLTableFooter::getType() == type || UIWidget::isType( type );
}
}} // namespace EE::UI

View File

@@ -6,6 +6,7 @@
#include <eepp/ui/uiconsole.hpp>
#include <eepp/ui/uidropdownlist.hpp>
#include <eepp/ui/uigridlayout.hpp>
#include <eepp/ui/uihtmltable.hpp>
#include <eepp/ui/uiimage.hpp>
#include <eepp/ui/uilinearlayout.hpp>
#include <eepp/ui/uilistbox.hpp>
@@ -43,7 +44,6 @@
#include <eepp/ui/uiviewpager.hpp>
#include <eepp/ui/uiwidgetcreator.hpp>
#include <eepp/ui/uiwidgettable.hpp>
#include <eepp/ui/uiwidgettablerow.hpp>
#include <eepp/ui/uiwindow.hpp>
namespace EE { namespace UI {
@@ -157,13 +157,22 @@ void UIWidgetCreator::createBaseWidgetList() {
registeredWidget["pre"] = UIRichText::NewPre;
registeredWidget["img"] = [] { return UIImage::NewWithTag( "img" ); };
registeredWidget["input"] = UITextInput::New;
registeredWidget["center"] = [] {
return UILinearLayout::NewVerticalWidthMatchParent( "center" );
};
registeredWidget["html"] = [] {
return UILinearLayout::NewVerticalWidthMatchParent( "html" );
};
registeredWidget["body"] = [] {
return UILinearLayout::NewVerticalWidthMatchParent( "body" );
};
registeredWidget["table"] = UIHTMLTable::New;
registeredWidget["tr"] = UIHTMLTableRow::New;
registeredWidget["thead"] = UIHTMLTableHead::New;
registeredWidget["tbody"] = UIHTMLTableBody::New;
registeredWidget["tfoot"] = UIHTMLTableFooter::New;
registeredWidget["th"] = [] { return UIHTMLTableCell::New( "th" ); };
registeredWidget["td"] = [] { return UIHTMLTableCell::New( "td" ); };
sBaseListCreated = true;
}

View File

@@ -9,6 +9,7 @@
#include <eepp/system/filesystem.hpp>
#include <eepp/system/scopedop.hpp>
#include <eepp/system/sys.hpp>
#include <eepp/ui/uihtmltable.hpp>
#include <eepp/ui/uiapplication.hpp>
#include <eepp/ui/uirichtext.hpp>
#include <eepp/ui/uiscenenode.hpp>
@@ -738,3 +739,100 @@ UTEST( UIRichText, WhitespaceCollapseCodeTest ) {
eeDelete( sceneNode );
Engine::destroySingleton();
}
UTEST( UIHTMLTable, basicLayout ) {
Engine::instance()->createWindow( WindowSettings( 800, 600, "HTML Table 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();
UI::UIThemeManager* themeManager = sceneNode->getUIThemeManager();
themeManager->setDefaultFont( font );
String xml = R"xml(
<table id="table" layout_width="400dp" layout_height="wrap_content">
<thead>
<tr>
<th>Header 1</th>
<th>Header 2</th>
</tr>
</thead>
<tbody>
<tr>
<td>Row 1 Col 1</td>
<td>Row 1 Col 2</td>
</tr>
<tr>
<td>Row 2 Col 1 which is very long and should cause wrapping if the table is narrow enough</td>
<td>Row 2 Col 2</td>
</tr>
</tbody>
</table>
)xml";
sceneNode->loadLayoutFromString( xml );
UI::UIHTMLTable* table = sceneNode->find<UI::UIHTMLTable>( "table" );
ASSERT_TRUE( table != nullptr );
// Force layout
sceneNode->update( Time::Zero );
// Check that we have rows and cells
int rowCount = 0;
std::function<void( Node* )> countRows = [&]( Node* node ) {
Node* child = node->getFirstChild();
while ( child ) {
if ( child->isWidget() ) {
UIWidget* widget = static_cast<UIWidget*>( child );
if ( widget->getType() == UI_TYPE_HTML_TABLE_ROW ) {
rowCount++;
} else if ( widget->getType() != UI_TYPE_HTML_TABLE ) {
countRows( widget );
}
}
child = child->getNextNode();
}
};
countRows( table );
EXPECT_EQ( rowCount, 3 );
// Verify that the table has a height greater than zero
EXPECT_GT( table->getPixelsSize().getHeight(), 0 );
// Check column synchronization
std::vector<UIHTMLTableRow*> rows;
std::function<void( Node* )> collectRows = [&]( Node* node ) {
Node* child = node->getFirstChild();
while ( child ) {
if ( child->isWidget() ) {
UIWidget* widget = static_cast<UIWidget*>( child );
if ( widget->getType() == UI_TYPE_HTML_TABLE_ROW ) {
rows.push_back( static_cast<UIHTMLTableRow*>( widget ) );
} else if ( widget->getType() != UI_TYPE_HTML_TABLE ) {
collectRows( widget );
}
}
child = child->getNextNode();
}
};
collectRows( table );
if ( rows.size() >= 2 ) {
Node* cell00 = rows[0]->getFirstChild();
Node* cell10 = rows[1]->getFirstChild();
if ( cell00 && cell10 && cell00->isWidget() && cell10->isWidget() ) {
EXPECT_EQ( cell00->asType<UIWidget>()->getPixelsPosition().x,
cell10->asType<UIWidget>()->getPixelsPosition().x );
}
}
eeDelete( sceneNode );
Engine::destroySingleton();
}