Minor fix in UIHTMLTable layouting.

Add *very* basic HTML test. And *very* basic HTML demo. This is more than basic, it's just that I need something to quickly test stuff.
This commit is contained in:
Martín Lucas Golini
2026-03-28 02:45:24 -03:00
parent fe815e97ac
commit a14b8f4de0
19 changed files with 882 additions and 43 deletions

View File

@@ -81,7 +81,7 @@ Uint32 Text::stringToStyleFlag( const std::string& str ) {
flags |= Text::Bold;
else if ( "italic" == cur )
flags |= Text::Italic;
else if ( "strikethrough" == cur )
else if ( "strikethrough" == cur || "line-through" == cur )
flags |= Text::StrikeThrough;
else if ( "shadowed" == cur || "shadow" == cur )
flags |= Text::Shadow;

View File

@@ -112,7 +112,7 @@ SyntaxColorScheme::Style parseStyle(
style.style |= Text::Italic;
else if ( "underline" == val || "underlined" == val )
style.style |= Text::Underlined;
else if ( "strikethrough" == val )
else if ( "strikethrough" == val || "line-through" == val )
style.style |= Text::StrikeThrough;
else if ( "shadow" == val )
style.style |= Text::Shadow;

View File

@@ -98,6 +98,7 @@ void UIHTMLTable::updateLayout() {
for ( Uint32 i = 0; i < end - start; ++i ) {
UIHTMLTableCell* cell = mCells[start + i];
cell->setLayoutWidthPolicy( SizePolicy::WrapContent );
cell->mSize.x = mSize.x;
cell->updateLayout();
Uint32 cellColspan = cell->getColspan();
if ( cellColspan == 1 ) {
@@ -216,7 +217,8 @@ void UIHTMLTable::updateLayout() {
mRows[rowCount - 1]->setPixelsPosition( mPaddingPx.Left, 0 );
if ( mHeightPolicy == SizePolicy::WrapContent ) {
setInternalPixelsHeight( headHeight + bodyHeight + footerHeight + mPaddingPx.Bottom );
setInternalPixelsHeight( mPaddingPx.Top + headHeight + bodyHeight + footerHeight +
mPaddingPx.Bottom );
}
mPacking = false;

View File

@@ -22,7 +22,9 @@
#include <eepp/ui/uitooltip.hpp>
#include <eepp/ui/uiwidgetcreator.hpp>
#include <eepp/ui/uiwindow.hpp>
#include <eepp/window/engine.hpp>
#include <eepp/window/window.hpp>
#define PUGIXML_HEADER_ONLY
#include <pugixml/pugixml.hpp>
@@ -318,7 +320,9 @@ std::vector<UIWidget*> UISceneNode::loadNode( pugi::xml_node node, Node* parent,
} else if ( String::iequals( widget.name(), "link" ) ) {
auto type = widget.attribute( "type" );
auto href = widget.attribute( "href" );
if ( !type.empty() && !href.empty() && String::iequals( type.value(), "text/css" ) ) {
auto rel = widget.attribute( "rel" );
if ( !href.empty() && ( String::iequals( type.value(), "text/css" ) ||
String::iequals( rel.value(), "stylesheet" ) ) ) {
loadCSS( href.as_string() );
}
}
@@ -1087,29 +1091,46 @@ void UISceneNode::loadFontFaces( const StyleSheetStyleVector& styles ) {
}
}
void UISceneNode::loadCSS( URI uri ) {
std::string scheme = uri.getScheme();
if ( !mURI.empty() && scheme.empty() ) {
std::string pathStart = mURI.getPath();
FileSystem::dirAddSlashAtEnd( pathStart );
std::string pathEnd = pathStart + uri.getPath();
uri = mURI;
uri.setPath( pathEnd );
}
URI UISceneNode::solveRelativePath( URI uri ) {
if ( mURI.empty() )
return uri;
if ( "file" == scheme || ( scheme.empty() && FileSystem::fileExists( uri.getPath() ) ) ) {
if ( mURI.getScheme().empty() )
uri.setScheme( "file" );
if ( uri.getPath().empty() || uri.getPath().back() != '/' )
uri.setPath( mURI.getPath() + uri.getPath() );
if ( uri.getScheme().empty() )
uri.setScheme( mURI.getScheme() );
if ( uri.getAuthority().empty() )
uri.setAuthority( mURI.getAuthority() );
return uri;
}
void UISceneNode::loadCSS( URI uri ) {
uri = solveRelativePath( uri );
std::string url = uri.toString();
Log::debug( "UISceneNode::loadCSS: %s", url );
if ( "file" == uri.getScheme() ||
( uri.getScheme().empty() && FileSystem::fileExists( uri.getPath() ) ) ) {
std::string filePath( uri.getPath() );
std::string css;
if ( FileSystem::fileExists( filePath ) && FileSystem::fileGet( filePath, css ) ) {
combineStyleSheet( css, true, String::hash( uri.toString() ) );
combineStyleSheet( css, true, String::hash( url ) );
Log::debug( "UISceneNode::loadCSS: Loaded - %s", url );
}
} else if ( "http" == scheme || "https" == scheme ) {
} else if ( "http" == uri.getScheme() || "https" == uri.getScheme() ) {
Http::getAsync(
[this, uri]( const Http&, Http::Request&, Http::Response& response ) {
[this, url]( const Http&, Http::Request&, Http::Response& response ) {
if ( !response.getBody().empty() ) {
std::string css( response.getBody() );
runOnMainThread( [css = std::move( css ), uri = std::move( uri ), this] {
combineStyleSheet( css, true, String::hash( uri.toString() ) );
runOnMainThread( [css = std::move( css ), url = std::move( url ), this] {
combineStyleSheet( css, true, String::hash( url ) );
Log::debug( "UISceneNode::loadCSS: Loaded - %s", url );
} );
}
},
@@ -1118,8 +1139,9 @@ void UISceneNode::loadCSS( URI uri ) {
IOStream* stream = VFS::instance()->getFileFromPath( uri.getPath() );
CSS::StyleSheetParser parser;
if ( parser.loadFromStream( *stream ) ) {
parser.getStyleSheet().setMarker( String::hash( uri.toString() ) );
parser.getStyleSheet().setMarker( String::hash( url ) );
combineStyleSheet( parser.getStyleSheet() );
Log::debug( "UISceneNode::loadCSS: Loaded - %s", url );
}
}
}
@@ -1242,4 +1264,10 @@ void UISceneNode::setURI( const URI& uri ) {
mURI = uri;
}
void UISceneNode::openURL( URI uri ) {
if ( mURLInterceptorCb && mURLInterceptorCb( uri ) )
return;
Engine::instance()->openURI( uri.toString() );
}
}} // namespace EE::UI

View File

@@ -341,7 +341,8 @@ bool UIScrollView::isTouchOverAllowedChildren() {
bool ret = mViewType == ScrollViewType::Outside
? !mVScroll->isMouseOverMeOrChildren() && !mHScroll->isMouseOverMeOrChildren()
: true;
return isMouseOverMeOrChildren() && mScrollView->isMouseOverMeOrChildren() && ret;
return isMouseOverMeOrChildren() && mScrollView && mScrollView->isMouseOverMeOrChildren() &&
ret;
}
std::string UIScrollView::getPropertyString( const PropertyDefinition* propertyDef,
@@ -437,7 +438,7 @@ bool UIScrollView::applyProperty( const StyleSheetProperty& attribute ) {
Uint32 UIScrollView::onMessage( const NodeMessage* Msg ) {
switch ( Msg->getMsg() ) {
case NodeMessage::MouseUp: {
if ( mVScroll->isEnabled() && 0 != mScrollView->getSize().getHeight() &&
if ( mScrollView && mVScroll->isEnabled() && 0 != mScrollView->getSize().getHeight() &&
isTouchOverAllowedChildren() && Msg->getSender()->isUINode() &&
!Msg->getSender()->asType<UINode>()->isScrollable() ) {
if ( Msg->getFlags() & EE_BUTTON_WUMASK ) {

View File

@@ -6,7 +6,6 @@
#include <eepp/ui/uitextspan.hpp>
#include <eepp/ui/uithememanager.hpp>
#include <eepp/ui/uiwidgetcreator.hpp>
#include <eepp/window/engine.hpp>
#define PUGIXML_HEADER_ONLY
#include <pugixml/pugixml.hpp>
@@ -574,7 +573,7 @@ Uint32 UIAnchorSpan::onMessage( const NodeMessage* Msg ) {
switch ( Msg->getMsg() ) {
case NodeMessage::MouseClick: {
if ( !mHref.empty() && ( Msg->getFlags() & EE_BUTTON_LMASK ) )
Engine::instance()->openURI( mHref );
getUISceneNode()->openURL( mHref );
return 1;
}
}
@@ -610,7 +609,7 @@ const std::string& UIAnchorSpan::getHref() const {
Uint32 UIAnchorSpan::onKeyDown( const KeyEvent& event ) {
if ( event.getKeyCode() == KEY_KP_ENTER || event.getKeyCode() == KEY_RETURN ) {
if ( !mHref.empty() ) {
Engine::instance()->openURI( mHref );
getUISceneNode()->openURL( mHref );
return 1;
}
}

View File

@@ -11,7 +11,7 @@
#include <eepp/ui/uitextview.hpp>
#include <eepp/ui/uithememanager.hpp>
#include <eepp/window/clipboard.hpp>
#include <eepp/window/engine.hpp>
#define PUGIXML_HEADER_ONLY
#include <pugixml/pugixml.hpp>
@@ -985,7 +985,7 @@ UIAnchor::UIAnchor( const std::string& tag ) : UITextView( tag ) {
onClick(
[this]( const MouseEvent* ) {
if ( !mHref.empty() )
Engine::instance()->openURI( mHref );
getUISceneNode()->openURL( mHref );
},
EE_BUTTON_LEFT );
}
@@ -1019,7 +1019,7 @@ const std::string& UIAnchor::getHref() const {
Uint32 UIAnchor::onKeyDown( const KeyEvent& event ) {
if ( event.getKeyCode() == KEY_KP_ENTER || event.getKeyCode() == KEY_RETURN ) {
if ( !mHref.empty() ) {
Engine::instance()->openURI( mHref );
getUISceneNode()->openURL( mHref );
return 1;
}
}

View File

@@ -166,16 +166,12 @@ void UIWidgetCreator::createBaseWidgetList() {
};
registeredWidget["center"] = [] {
auto center = UIRichText::NewWithTag( "center" );
center->setLayoutWidthPolicy( SizePolicy::WrapContent );
// center->setLayoutWidthPolicy( SizePolicy::WrapContent );
return center;
};
registeredWidget["html"] = [] {
return UILinearLayout::NewVerticalWidthMatchParent( "html" );
};
registeredWidget["html"] = [] { return UIRichText::NewWithTag( "html" ); };
registeredWidget["head"] = [] { return UIWidget::NewWithTag( "head" ); };
registeredWidget["body"] = [] {
return UILinearLayout::NewVerticalWidthMatchParent( "body" );
};
registeredWidget["body"] = [] { return UIRichText::NewWithTag( "body" ); };
registeredWidget["table"] = UIHTMLTable::New;
registeredWidget["tr"] = UIHTMLTableRow::New;
registeredWidget["thead"] = UIHTMLTableHead::New;

View File

@@ -0,0 +1,93 @@
#include <eepp/ee.hpp>
EE_MAIN_FUNC int main( int, char** ) {
UIApplication app( { 1280, 720, "eepp - UI HTML Example" } );
Log::instance()->setLogLevelThreshold( LogLevel::Debug );
Log::instance()->setLogToStdOut( true );
Log::instance()->setLiveWrite( true );
auto win = app.getWindow();
auto ui = app.getUI();
ui->setColorSchemePreference( ColorSchemeExtPreference::Light );
ui->loadLayoutFromString( R"xml(
<vbox layout_width="match_parent" layout_height="match_parent">
<hbox layout_width="match_parent" layout_height="wrap_content">
<TextInput id="url_bar" layout_width="0" layout_weight="1"
hint="@string(enter_address, Enter Address)" />
</hbox>
<ScrollView id="html_view" layout_width="match_parent" layout_height="0" layout_weight="1">
<vbox layout_width="match_parent" layout_height="wrap_content" id="html_doc"></vbox>
</ScrollView>
</vbox>
)xml" );
auto urlBar = ui->find( "url_bar" )->asType<UITextInput>();
auto mainContainer = ui->find( "html_doc" );
const auto loadDocument = [&]( URI url ) {
static String::HashType prevURL = 0;
std::string data;
if ( !url.getScheme().empty() ) {
if ( url.getScheme() == "https" || url.getScheme() == "http" ) {
auto response = Http::get( url, Seconds( 5 ) );
data = response.getBody();
} else if ( url.getScheme() == "file" ) {
FileSystem::fileGet( url.getPath(), data );
}
} else if ( !url.getPath().empty() && url.getPath().front() == '/' ) {
FileSystem::fileGet( url.getPath(), data );
}
if ( !data.empty() ) {
if ( url.getPath().empty() || url.getPath().back() != '/' ) {
if ( url.getScheme() == "file" &&
!FileSystem::fileExtension( url.getPath() ).empty() ) {
url.setPath( FileSystem::fileRemoveFileName( url.getPath() ) );
}
url.setPath( url.getPath() + "/" );
}
mainContainer->closeAllChildren();
if ( prevURL )
ui->getStyleSheet().removeAllWithMarker( prevURL );
ui->setURI( url );
auto hash = String::hash( url.toString() );
ui->loadLayoutFromString( data, mainContainer, hash );
prevURL = hash;
}
};
urlBar->on( Event::OnPressEnter,
[&]( auto event ) { loadDocument( urlBar->getText().toUtf8() ); } );
ui->setURLInterceptorCb( [&]( URI uri ) {
loadDocument( ui->solveRelativePath( uri ) );
return true;
} );
win->getInput()->pushCallback( [&loadDocument]( InputEvent* event ) {
switch ( event->Type ) {
case InputEvent::FileDropped: {
std::string file( event->file.file );
loadDocument( "file://" + file );
break;
}
case InputEvent::TextDropped: {
loadDocument( event->textdrop.text );
break;
}
default:
break;
}
} );
app.getUI()->on( Event::KeyUp, [&app]( const Event* event ) {
if ( event->asKeyEvent()->getKeyCode() == KEY_F11 ) {
UIWidgetInspector::create( app.getUI() );
}
} );
return app.run();
}

View File

@@ -0,0 +1,90 @@
// #include "compareimages.hpp"
#include "utest.h"
#include <eepp/graphics/fontfamily.hpp>
#include <eepp/graphics/fonttruetype.hpp>
#include <eepp/scene/scenemanager.hpp>
#include <eepp/system/filesystem.hpp>
#include <eepp/system/sys.hpp>
#include <eepp/ui/tools/uiwidgetinspector.hpp>
#include <eepp/ui/uihtmltable.hpp>
#include <eepp/ui/uiscenenode.hpp>
#include <eepp/ui/uitextspan.hpp>
#include <eepp/ui/uithememanager.hpp>
#include <eepp/window/engine.hpp>
#include <eepp/window/input.hpp>
using namespace EE;
using namespace EE::Graphics;
using namespace EE::Window;
using namespace EE::Scene;
using namespace EE::UI;
using namespace EE::UI::Tools;
UTEST( UIHTMLTable, complexLayout ) {
auto win = Engine::instance()->createWindow(
WindowSettings( 1024, 650, "HTML Tables Test", WindowStyle::Default, WindowBackend::Default,
32, {}, 1, false, true ),
ContextSettings( false, ContextSettings::FrameRateLimitScreenRefreshRate, 4 ) );
FileSystem::changeWorkingDirectory( Sys::getProcessPath() );
#ifdef EE_DEBUG
Log::instance()->setLiveWrite( true );
Log::instance()->setLogToStdOut( true );
#endif
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->setURI( Sys::getProcessPath() + "assets/html/" );
sceneNode->loadLayoutFromFile( "assets/html/hn_thread_test.html" );
win->setClearColor( Color::White );
while ( win->isRunning() ) {
win->getInput()->update();
SceneManager::instance()->update();
win->clear();
SceneManager::instance()->draw();
win->display();
}
auto hnMain = sceneNode->getRoot()->find( "hnmain" );
auto bigbox = sceneNode->getRoot()->find( "bigbox" );
auto commentTree = sceneNode->getRoot()->findByClass( "comment-tree" );
auto votelinks = sceneNode->getRoot()->findByClass( "votelinks" );
auto commentTd = sceneNode->getRoot()->findByClass( "default" );
auto comment = sceneNode->getRoot()->findByClass( "comment" );
auto commtext = sceneNode->getRoot()->findByClass( "commtext" );
EXPECT_GT( votelinks->getPixelsSize().getWidth(), 0 );
EXPECT_GT( votelinks->getPixelsSize().getHeight(), 0 );
EXPECT_GT( commentTree->getPixelsSize().getWidth(), 0 );
EXPECT_GT( commentTree->getPixelsSize().getHeight(), 0 );
EXPECT_GT( comment->getPixelsSize().getWidth(), 0 );
EXPECT_GT( commtext->getPixelsSize().getWidth(), 0 );
EXPECT_GT( commentTd->getPixelsSize().getWidth(), 0 );
EXPECT_GT( commentTd->getPixelsSize().getHeight(), 0 );
EXPECT_GE( hnMain->getPixelsSize().getHeight(), bigbox->getPixelsSize().getHeight() );
Float totalTds = commentTd->getPixelsSize().getWidth() + votelinks->getPixelsSize().getWidth();
Float mainTotal = hnMain->getPixelsSize().getWidth();
EXPECT_GT( totalTds, 0 );
EXPECT_GT( mainTotal, 0 );
// EXPECT_LT( totalTds, mainTotal );
// compareImages( utest_state, utest_result, win, "eepp-uihtmltable-complex-layout", "html" );
Engine::destroySingleton();
}