mirror of
https://github.com/SpartanJ/eepp.git
synced 2026-05-28 17:16:29 +03:00
Add basic HTML tables support, this allows us to display tables in Markdown files.
This commit is contained in:
@@ -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).*
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
82
include/eepp/ui/uihtmltable.hpp
Normal file
82
include/eepp/ui/uihtmltable.hpp
Normal 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
|
||||
@@ -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
283
src/eepp/ui/uihtmltable.cpp
Normal 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
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user