mirror of
https://github.com/SpartanJ/eepp.git
synced 2026-09-22 13:01:05 +03:00
6485 lines
244 KiB
C++
6485 lines
244 KiB
C++
#include "compareimages.hpp"
|
|
#include "utest.hpp"
|
|
|
|
#include <eepp/graphics/fontfamily.hpp>
|
|
#include <eepp/graphics/fonttruetype.hpp>
|
|
#include <eepp/graphics/image.hpp>
|
|
#include <eepp/graphics/renderer/renderer.hpp>
|
|
#include <eepp/graphics/resourcescope.hpp>
|
|
#include <eepp/graphics/texturedrawable.hpp>
|
|
#include <eepp/graphics/texturefactory.hpp>
|
|
#include <eepp/graphics/textureregion.hpp>
|
|
#include <eepp/scene/keyevent.hpp>
|
|
#include <eepp/scene/scenemanager.hpp>
|
|
#include <eepp/system/filesystem.hpp>
|
|
#include <eepp/system/sys.hpp>
|
|
#include <eepp/ui/css/stylesheetparser.hpp>
|
|
#include <eepp/ui/css/stylesheetselector.hpp>
|
|
#include <eepp/ui/css/stylesheetspecification.hpp>
|
|
#include <eepp/ui/iconmanager.hpp>
|
|
#include <eepp/ui/tools/htmlformatter.hpp>
|
|
#include <eepp/ui/tools/uiwidgetinspector.hpp>
|
|
#include <eepp/ui/uiapplication.hpp>
|
|
#include <eepp/ui/uiborderdrawable.hpp>
|
|
#include <eepp/ui/uicheckbox.hpp>
|
|
#include <eepp/ui/uicodeeditor.hpp>
|
|
#include <eepp/ui/uihtmldetails.hpp>
|
|
#include <eepp/ui/uihtmlimage.hpp>
|
|
#include <eepp/ui/uihtmlinput.hpp>
|
|
#include <eepp/ui/uihtmlliststyle.hpp>
|
|
#include <eepp/ui/uihtmltable.hpp>
|
|
#include <eepp/ui/uihtmltextarea.hpp>
|
|
#include <eepp/ui/uihtmltextinput.hpp>
|
|
#include <eepp/ui/uiiconthememanager.hpp>
|
|
#include <eepp/ui/uimarkdownview.hpp>
|
|
#include <eepp/ui/uinodedrawable.hpp>
|
|
#include <eepp/ui/uiradiobutton.hpp>
|
|
#include <eepp/ui/uirichtext.hpp>
|
|
#include <eepp/ui/uiscenenode.hpp>
|
|
#include <eepp/ui/uiscrollbar.hpp>
|
|
#include <eepp/ui/uitextnode.hpp>
|
|
#include <eepp/ui/uitextspan.hpp>
|
|
#include <eepp/ui/uitheme.hpp>
|
|
#include <eepp/ui/uithememanager.hpp>
|
|
#include <eepp/ui/uiwebview.hpp>
|
|
#include <eepp/ui/uiwindow.hpp>
|
|
#include <eepp/window/engine.hpp>
|
|
#include <eepp/window/input.hpp>
|
|
|
|
#include <atomic>
|
|
#include <cstdlib>
|
|
#include <iostream>
|
|
|
|
using namespace EE;
|
|
using namespace EE::Graphics;
|
|
using namespace EE::Window;
|
|
using namespace EE::Scene;
|
|
using namespace EE::UI;
|
|
using namespace EE::UI::Tools;
|
|
|
|
static Texture* getDrawableTexture( Drawable* drawable ) {
|
|
if ( !drawable )
|
|
return nullptr;
|
|
switch ( drawable->getDrawableType() ) {
|
|
case Drawable::TEXTURE:
|
|
return static_cast<Texture*>( drawable );
|
|
case Drawable::TEXTUREDRAWABLE:
|
|
return static_cast<TextureDrawable*>( drawable )->getTexture().get();
|
|
case Drawable::TEXTUREREGION:
|
|
return static_cast<TextureRegion*>( drawable )->getTexture().get();
|
|
default:
|
|
return nullptr;
|
|
}
|
|
}
|
|
|
|
static Texture* getDrawableTexture( const DrawablePtr& drawable ) {
|
|
return getDrawableTexture( drawable.get() );
|
|
}
|
|
|
|
static void init_ui_test() {
|
|
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" ).get();
|
|
font->loadFromFile( "../assets/fonts/NotoSans-Regular.ttf" );
|
|
FontFamily::loadFromRegular( font );
|
|
FontTrueType* monospace = FontTrueType::New( "monospace" ).get();
|
|
monospace->loadFromFile( "../assets/fonts/DejaVuSansMono.ttf" );
|
|
FontFamily::loadFromRegular( monospace );
|
|
|
|
UISceneNode* sceneNode = UISceneNode::New();
|
|
SceneManager::instance()->add( sceneNode );
|
|
sceneNode->setColorSchemePreference( ColorSchemePreference::Light );
|
|
UI::UIThemeManager* themeManager = sceneNode->getUIThemeManager();
|
|
themeManager->setDefaultFont( font );
|
|
}
|
|
|
|
static String uiHtmlRenderedText( const RichText& richText ) {
|
|
String text;
|
|
const auto& lines = richText.getLines();
|
|
for ( size_t lineIndex = 0; lineIndex < lines.size(); ++lineIndex ) {
|
|
if ( lineIndex > 0 )
|
|
text += '\n';
|
|
for ( const auto& span : lines[lineIndex].spans ) {
|
|
if ( span.type == RichText::RenderSpan::Type::Text && span.text )
|
|
text += span.text->getString();
|
|
}
|
|
}
|
|
return text;
|
|
}
|
|
|
|
static String uiHtmlLineText( const RichText& richText, size_t lineIndex ) {
|
|
String text;
|
|
const auto& lines = richText.getLines();
|
|
if ( lineIndex >= lines.size() )
|
|
return text;
|
|
for ( const auto& span : lines[lineIndex].spans ) {
|
|
if ( span.type == RichText::RenderSpan::Type::Text && span.text )
|
|
text += span.text->getString();
|
|
}
|
|
while ( !text.empty() && ( text.front() == '\n' || text.front() == '\r' ) )
|
|
text = text.substr( 1 );
|
|
while ( !text.empty() && ( text.back() == '\n' || text.back() == '\r' ) )
|
|
text.pop_back();
|
|
return text;
|
|
}
|
|
|
|
UTEST( UIHTMLTable, complexLayout ) {
|
|
auto win = Engine::instance()->createWindow(
|
|
WindowSettings( 1024, 653, "HTML Tables Test", VisualTestWindowStyle,
|
|
WindowBackend::Default, 32, {}, 1, false, true ),
|
|
ContextSettings( false, 0, 0, GLv_default, true, false ) );
|
|
FileSystem::changeWorkingDirectory( Sys::getProcessPath() );
|
|
|
|
FontTrueType* font = FontTrueType::New( "NotoSans-Regular" ).get();
|
|
font->loadFromFile( "../assets/fonts/NotoSans-Regular.ttf" );
|
|
ASSERT_TRUE( font != nullptr && font->loaded() );
|
|
FontFamily::loadFromRegular( font );
|
|
|
|
UISceneNode* sceneNode = UISceneNode::New();
|
|
SceneManager::instance()->add( sceneNode );
|
|
UI::UIThemeManager* themeManager = sceneNode->getUIThemeManager();
|
|
themeManager->setDefaultFont( font );
|
|
sceneNode->setURI( "file://" + Sys::getProcessPath() + "assets/html/" );
|
|
std::string html;
|
|
FileSystem::fileGet( "assets/html/hn_thread_test.html", html );
|
|
sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( 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( 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_NEAR( totalTds, mainTotal, 0.1 );
|
|
compareImages( utest_state, utest_result, win, "eepp-uihtmltable-complex-layout", "html" );
|
|
|
|
Engine::destroySingleton();
|
|
}
|
|
|
|
UTEST( UIHTMLTable, complexLayout2 ) {
|
|
auto win = Engine::instance()->createWindow(
|
|
WindowSettings( 1024, 650, "HTML Tables Test 2", VisualTestWindowStyle,
|
|
WindowBackend::Default, 32, {}, 1, false, true ),
|
|
ContextSettings( false, 0, 0, GLv_default, true, false ) );
|
|
FileSystem::changeWorkingDirectory( Sys::getProcessPath() );
|
|
|
|
FontTrueType* font = FontTrueType::New( "NotoSans-Regular" ).get();
|
|
font->loadFromFile( "../assets/fonts/NotoSans-Regular.ttf" );
|
|
ASSERT_TRUE( font != nullptr && font->loaded() );
|
|
FontFamily::loadFromRegular( font );
|
|
|
|
UISceneNode* sceneNode = UISceneNode::New();
|
|
SceneManager::instance()->add( sceneNode );
|
|
UI::UIThemeManager* themeManager = sceneNode->getUIThemeManager();
|
|
themeManager->setDefaultFont( font );
|
|
sceneNode->setURI( "file://" + Sys::getProcessPath() + "assets/html/" );
|
|
std::string html;
|
|
FileSystem::fileGet( "assets/html/hn_threaded_test.html", html );
|
|
// Keep this fixture close to the captured viewport. Apple Software Renderer drops the
|
|
// large offscreen solid-background quad if the original full HN thread is used, even
|
|
// though GPU renderers and other software renderers rasterize it correctly.
|
|
sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( html ) );
|
|
win->setClearColor( Color::White );
|
|
|
|
const Color expectedMainBg( "#f6f6ef" );
|
|
UIWidget* hnMain = nullptr;
|
|
for ( int i = 0; i < 8; i++ ) {
|
|
win->getInput()->update();
|
|
SceneManager::instance()->update();
|
|
|
|
if ( !hnMain )
|
|
hnMain = sceneNode->getRoot()->find( "hnmain" )->asType<UIWidget>();
|
|
|
|
win->clear();
|
|
SceneManager::instance()->draw();
|
|
win->display();
|
|
|
|
if ( hnMain && hnMain->getBackgroundColor() == expectedMainBg )
|
|
break;
|
|
}
|
|
|
|
ASSERT_TRUE( hnMain != nullptr );
|
|
EXPECT_STDSTREQ( hnMain->getBackgroundColor().toHexString(), expectedMainBg.toHexString() );
|
|
|
|
compareImages( utest_state, utest_result, win, "eepp-uihtmltable-complex-layout-2", "html" );
|
|
|
|
Engine::destroySingleton();
|
|
}
|
|
|
|
UTEST( UIHTML, redditOldThreadWebViewSmoke ) {
|
|
auto win = Engine::instance()->createWindow(
|
|
WindowSettings( 1024, 1400, "Old Reddit Thread Test", VisualTestWindowStyle,
|
|
WindowBackend::Default, 32, {}, 1, false, true ),
|
|
ContextSettings( false, 0, 0, GLv_default, true, false ) );
|
|
FileSystem::changeWorkingDirectory( Sys::getProcessPath() );
|
|
|
|
if ( !FileSystem::fileExists( Sys::getProcessPath() +
|
|
"assets/html/reddit_old_thread_files/reddit.ETA_etA2z5U.css" ) ) {
|
|
|
|
Engine::destroySingleton();
|
|
UTEST_SKIP( "old Reddit fixture CSS asset is not readable" );
|
|
}
|
|
|
|
FontTrueType* font = FontTrueType::New( "NotoSans-Regular" ).get();
|
|
font->loadFromFile( "../assets/fonts/NotoSans-Regular.ttf" );
|
|
ASSERT_TRUE( font != nullptr && font->loaded() );
|
|
FontFamily::loadFromRegular( font );
|
|
|
|
UISceneNode* sceneNode = UISceneNode::New();
|
|
SceneManager::instance()->add( sceneNode );
|
|
UI::UIThemeManager* themeManager = sceneNode->getUIThemeManager();
|
|
themeManager->setDefaultFont( font );
|
|
|
|
UIWebView* webView = UIWebView::New();
|
|
webView->setParent( sceneNode->getRoot() );
|
|
webView->setPixelsSize( win->getWidth(), win->getHeight() );
|
|
webView->setLayoutSizePolicy( SizePolicy::Fixed, SizePolicy::Fixed );
|
|
webView->loadURI(
|
|
URI( "file://" + Sys::getProcessPath() + "assets/html/reddit_old_thread.html" ) );
|
|
|
|
win->setClearColor( Color::White );
|
|
for ( int i = 0; i < 8; i++ ) {
|
|
win->getInput()->update();
|
|
SceneManager::instance()->update();
|
|
win->clear();
|
|
SceneManager::instance()->draw();
|
|
win->display();
|
|
}
|
|
|
|
UISceneNode* documentScene = webView->getDocumentSceneNode();
|
|
ASSERT_TRUE( documentScene != nullptr );
|
|
UIWidget* documentRoot = documentScene->getRoot();
|
|
ASSERT_TRUE( documentRoot != nullptr );
|
|
EXPECT_TRUE( sceneNode->getRoot()->findByClass( "side" ) == nullptr );
|
|
|
|
auto side = documentRoot->findByClass( "side" );
|
|
auto siteTable = documentRoot->find( "siteTable" );
|
|
auto midcol = documentRoot->findByClass( "midcol" );
|
|
auto entry = documentRoot->findByClass( "entry" );
|
|
auto arrow = documentRoot->findByClass( "arrow" );
|
|
auto srHeader = documentRoot->find( "sr-header-area" );
|
|
auto redesignButton = documentRoot->find( "redesign-beta-optin-btn" );
|
|
auto srDrop = documentRoot->querySelector( "#sr-header-area .dropdown.srdrop" );
|
|
auto srList = documentRoot->querySelector( "#sr-header-area .sr-list" );
|
|
auto srFlatList = documentRoot->querySelector( "#sr-header-area .sr-list .flat-list" );
|
|
auto headerBottomLeft = documentRoot->find( "header-bottom-left" );
|
|
auto headerBottomRight = documentRoot->find( "header-bottom-right" );
|
|
auto header = documentRoot->find( "header" );
|
|
auto dropChoices = documentRoot->querySelector( ".drop-choices.srdrop" );
|
|
auto selftextMd = documentRoot->querySelector( ".link .usertext-body .md" );
|
|
auto selftextFirstP = documentRoot->querySelector( ".link .usertext-body .md p" );
|
|
auto postTitle = documentRoot->querySelector( ".link .top-matter > p.title" );
|
|
auto postTitleAnchor = documentRoot->querySelector( ".link .top-matter > p.title > a.title" );
|
|
auto postDomain = documentRoot->querySelector( ".link .top-matter > p.title > .domain" );
|
|
auto postTagline = documentRoot->querySelector( ".link .top-matter > p.tagline" );
|
|
auto postExpando = documentRoot->querySelector( ".link .expando" );
|
|
auto postUsertext = documentRoot->querySelector( ".link .expando > form.usertext" );
|
|
auto commentArea = documentRoot->querySelector( ".commentarea" );
|
|
auto commentForm = documentRoot->find( "form-t3_1tk9dgh5ov" );
|
|
auto commentEdit = documentRoot->querySelector( ".commentarea .usertext-edit" );
|
|
auto commentTextarea = documentRoot->querySelector( ".commentarea textarea" );
|
|
auto commentHelpToggle = documentRoot->querySelector( ".commentarea .help-toggle" );
|
|
auto commentContentPolicy = documentRoot->querySelector( ".commentarea a.reddiquette" );
|
|
auto flairCheckbox = documentRoot->find( "flair_enabled" );
|
|
auto commentButtons =
|
|
documentRoot->querySelector( "#thing_t1_on791mh > .entry > .flat-list.buttons" );
|
|
auto redditFooter = documentRoot->querySelector( ".footer-parent > .footer" );
|
|
auto fancyToggle = documentRoot->querySelector( ".titlebox .fancy-toggle-button" );
|
|
auto leaveButton =
|
|
documentRoot->querySelector( ".titlebox .fancy-toggle-button .option.active.remove" );
|
|
auto searchText = documentRoot->querySelector( "#search input[type=text]" );
|
|
auto searchSubmit = documentRoot->querySelector( "#search input[type=submit]" );
|
|
auto shortlinkText = documentRoot->find( "shortlink-text" );
|
|
|
|
ASSERT_TRUE( side != nullptr );
|
|
ASSERT_TRUE( siteTable != nullptr );
|
|
ASSERT_TRUE( midcol != nullptr );
|
|
ASSERT_TRUE( entry != nullptr );
|
|
ASSERT_TRUE( arrow != nullptr );
|
|
ASSERT_TRUE( srHeader != nullptr );
|
|
ASSERT_TRUE( redesignButton != nullptr );
|
|
ASSERT_TRUE( srDrop != nullptr );
|
|
ASSERT_TRUE( srList != nullptr );
|
|
ASSERT_TRUE( srFlatList != nullptr );
|
|
ASSERT_TRUE( headerBottomLeft != nullptr );
|
|
ASSERT_TRUE( headerBottomRight != nullptr );
|
|
ASSERT_TRUE( header != nullptr );
|
|
ASSERT_TRUE( dropChoices != nullptr );
|
|
ASSERT_TRUE( selftextMd != nullptr );
|
|
ASSERT_TRUE( selftextFirstP != nullptr );
|
|
ASSERT_TRUE( postTitle != nullptr );
|
|
ASSERT_TRUE( postTitleAnchor != nullptr );
|
|
ASSERT_TRUE( postDomain != nullptr );
|
|
ASSERT_TRUE( postTagline != nullptr );
|
|
ASSERT_TRUE( postExpando != nullptr );
|
|
ASSERT_TRUE( postUsertext != nullptr );
|
|
ASSERT_TRUE( commentArea != nullptr );
|
|
ASSERT_TRUE( commentForm != nullptr );
|
|
ASSERT_TRUE( commentEdit != nullptr );
|
|
ASSERT_TRUE( commentTextarea != nullptr );
|
|
ASSERT_TRUE( commentHelpToggle != nullptr );
|
|
ASSERT_TRUE( commentContentPolicy != nullptr );
|
|
ASSERT_TRUE( flairCheckbox != nullptr );
|
|
ASSERT_TRUE( commentButtons != nullptr );
|
|
ASSERT_TRUE( redditFooter != nullptr );
|
|
ASSERT_TRUE( fancyToggle != nullptr );
|
|
ASSERT_TRUE( leaveButton != nullptr );
|
|
ASSERT_TRUE( searchText != nullptr );
|
|
ASSERT_TRUE( searchSubmit != nullptr );
|
|
ASSERT_TRUE( shortlinkText != nullptr );
|
|
EXPECT_LT( shortlinkText->asType<UIWidget>()->getPixelsSize().getHeight(), 30.f );
|
|
EXPECT_EQ( searchText->asType<UIHTMLWidget>()->getBaselineAlign().type,
|
|
CSSBaselineAlignment::Middle );
|
|
EXPECT_EQ( searchSubmit->asType<UIHTMLWidget>()->getBaselineAlign().type,
|
|
CSSBaselineAlignment::Middle );
|
|
EXPECT_STRINGEQ( leaveButton->asType<UITextSpan>()->getText(), "leave" );
|
|
EXPECT_GT( fancyToggle->getPixelsSize().getWidth(), 0.f );
|
|
EXPECT_GT( fancyToggle->getPixelsSize().getHeight(), 0.f );
|
|
const Vector2f searchTextWorld =
|
|
searchText->asType<UIWidget>()->convertToWorldSpace( Vector2f::Zero );
|
|
const Vector2f searchSubmitWorld =
|
|
searchSubmit->asType<UIWidget>()->convertToWorldSpace( Vector2f::Zero );
|
|
EXPECT_NEAR( searchTextWorld.y + searchText->getPixelsSize().getHeight() * 0.5f,
|
|
searchSubmitWorld.y + searchSubmit->getPixelsSize().getHeight() * 0.5f, 1.f );
|
|
EXPECT_EQ( redditFooter->asType<UIHTMLWidget>()->getDisplay(), CSSDisplay::Flex );
|
|
Float footerColumnRight = 0.f;
|
|
for ( auto* col : redditFooter->findAllByClass( "col" ) ) {
|
|
EXPECT_GE( col->getPixelsPosition().x, footerColumnRight );
|
|
footerColumnRight = col->getPixelsPosition().x + col->getPixelsSize().getWidth();
|
|
Float footerItemBottom = 0.f;
|
|
for ( auto* li : col->querySelectorAll( "li" ) ) {
|
|
EXPECT_GE( li->getPixelsPosition().y, footerItemBottom );
|
|
footerItemBottom = li->getPixelsPosition().y + li->getPixelsSize().getHeight();
|
|
}
|
|
}
|
|
Float commentButtonRight = 0.f;
|
|
for ( auto* li : commentButtons->findAllByTag( "li" ) ) {
|
|
ASSERT_TRUE( li->findByTag( "a" ) != nullptr );
|
|
EXPECT_GE( li->getPixelsPosition().x, commentButtonRight );
|
|
commentButtonRight = li->getPixelsPosition().x + li->getPixelsSize().getWidth();
|
|
}
|
|
EXPECT_GE( postDomain->getPixelsPosition().x, postTitleAnchor->getPixelsPosition().x +
|
|
postTitleAnchor->getPixelsSize().getWidth() );
|
|
|
|
UIWidget* content =
|
|
siteTable->getParent()->isWidget() ? siteTable->getParent()->asType<UIWidget>() : nullptr;
|
|
ASSERT_TRUE( content != nullptr );
|
|
Vector2f midcolPos = midcol->asType<UIWidget>()->convertToWorldSpace( { 0, 0 } );
|
|
Vector2f arrowPos = arrow->asType<UIWidget>()->convertToWorldSpace( { 0, 0 } );
|
|
Vector2f srListPos = srList->asType<UIWidget>()->convertToWorldSpace( { 0, 0 } );
|
|
Vector2f srHeaderPos = srHeader->asType<UIWidget>()->convertToWorldSpace( { 0, 0 } );
|
|
Vector2f srDropPos = srDrop->asType<UIWidget>()->convertToWorldSpace( { 0, 0 } );
|
|
Vector2f srFlatListPos = srFlatList->asType<UIWidget>()->convertToWorldSpace( { 0, 0 } );
|
|
Vector2f headerBottomLeftPos =
|
|
headerBottomLeft->asType<UIWidget>()->convertToWorldSpace( { 0, 0 } );
|
|
Vector2f headerBottomRightPos =
|
|
headerBottomRight->asType<UIWidget>()->convertToWorldSpace( { 0, 0 } );
|
|
Vector2f headerPos = header->asType<UIWidget>()->convertToWorldSpace( { 0, 0 } );
|
|
|
|
Vector2f commentHelpTogglePos =
|
|
commentHelpToggle->asType<UIWidget>()->convertToWorldSpace( { 0, 0 } );
|
|
Vector2f commentContentPolicyPos =
|
|
commentContentPolicy->asType<UIWidget>()->convertToWorldSpace( { 0, 0 } );
|
|
Vector2f commentEditPos = commentEdit->asType<UIWidget>()->convertToWorldSpace( { 0, 0 } );
|
|
const Float midcolCenter =
|
|
midcolPos.x + midcol->asType<UIWidget>()->getPixelsSize().getWidth() / 2.f;
|
|
const Float arrowCenter =
|
|
arrowPos.x + arrow->asType<UIWidget>()->getPixelsSize().getWidth() / 2.f;
|
|
EXPECT_NEAR( midcolCenter, arrowCenter, 1.f );
|
|
EXPECT_NEAR( srListPos.y, srHeaderPos.y, 1.f );
|
|
EXPECT_GE( srListPos.x,
|
|
srDropPos.x + srDrop->asType<UIWidget>()->getPixelsSize().getWidth() - 1.f );
|
|
EXPECT_LE( srList->asType<UIWidget>()->getPixelsSize().getWidth(),
|
|
win->getWidth() - srListPos.x + 1.f );
|
|
EXPECT_LE( srList->asType<UIWidget>()->getPixelsSize().getHeight(),
|
|
srHeader->asType<UIWidget>()->getPixelsSize().getHeight() + 1.f );
|
|
EXPECT_EQ( srFlatList->asType<UIRichText>()->getRichTextPtr()->getLines().size(), (size_t)1 );
|
|
EXPECT_NEAR( srFlatListPos.y, srHeaderPos.y, 1.f );
|
|
EXPECT_GE( headerBottomLeftPos.y,
|
|
srHeaderPos.y + srHeader->asType<UIWidget>()->getPixelsSize().getHeight() - 1.f );
|
|
EXPECT_EQ( headerBottomRight->asType<UIHTMLWidget>()->getCSSPosition(), CSSPosition::Absolute );
|
|
EXPECT_NEAR( headerBottomRightPos.x +
|
|
headerBottomRight->asType<UIWidget>()->getPixelsSize().getWidth(),
|
|
headerPos.x + header->asType<UIWidget>()->getPixelsSize().getWidth(), 1.f );
|
|
EXPECT_NEAR( headerBottomRightPos.y +
|
|
headerBottomRight->asType<UIWidget>()->getPixelsSize().getHeight(),
|
|
headerPos.y + header->asType<UIWidget>()->getPixelsSize().getHeight() -
|
|
header->asType<UIWidget>()->getPixelsContentOffset().Bottom,
|
|
1.f );
|
|
EXPECT_FALSE( dropChoices->asType<UIWidget>()->isVisible() );
|
|
EXPECT_EQ( dropChoices->asType<UIHTMLWidget>()->getDisplay(), CSSDisplay::None );
|
|
ASSERT_TRUE( flairCheckbox->isType( UI_TYPE_HTML_INPUT ) );
|
|
ASSERT_TRUE( flairCheckbox->asType<UIHTMLInput>()->getChildWidget() != nullptr );
|
|
EXPECT_TRUE(
|
|
flairCheckbox->asType<UIHTMLInput>()->getChildWidget()->asType<UICheckBox>()->isChecked() );
|
|
EXPECT_NEAR(
|
|
commentHelpTogglePos.x + commentHelpToggle->asType<UIWidget>()->getPixelsSize().getWidth(),
|
|
commentEditPos.x + commentEdit->asType<UIWidget>()->getPixelsSize().getWidth(), 1.f );
|
|
EXPECT_LE( commentContentPolicyPos.x +
|
|
commentContentPolicy->asType<UIWidget>()->getPixelsSize().getWidth(),
|
|
commentHelpTogglePos.x -
|
|
commentHelpToggle->asType<UIWidget>()->getLayoutPixelsMargin().Left + 1.f );
|
|
|
|
auto* arrowBackground = arrow->asType<UIWidget>()->getBackground();
|
|
ASSERT_TRUE( arrowBackground != nullptr );
|
|
auto* arrowBackgroundLayer = arrowBackground->getLayer( 0 );
|
|
ASSERT_TRUE( arrowBackgroundLayer != nullptr );
|
|
ASSERT_TRUE( arrowBackgroundLayer->getDrawable() != nullptr );
|
|
EXPECT_EQ( arrowBackgroundLayer->getDrawable()->getPixelsSize().getWidth(), 140 );
|
|
EXPECT_EQ( arrowBackgroundLayer->getDrawable()->getPixelsSize().getHeight(), 1751 );
|
|
EXPECT_STDSTREQ( arrowBackgroundLayer->getPositionX(), "-42px" );
|
|
EXPECT_STDSTREQ( arrowBackgroundLayer->getPositionY(), "-1678px" );
|
|
EXPECT_NEAR( arrowBackgroundLayer->getOffset().x, -42.f, 0.1f );
|
|
EXPECT_NEAR( arrowBackgroundLayer->getOffset().y, -1678.f, 0.1f );
|
|
|
|
compareImages( utest_state, utest_result, win, "eepp-ui-reddit-old-thread", "html" );
|
|
|
|
Engine::destroySingleton();
|
|
}
|
|
|
|
UTEST( UIHTML, StrikeElementUsesDefaultLineThrough ) {
|
|
init_ui_test();
|
|
UISceneNode* sceneNode = SceneManager::instance()->getUISceneNode();
|
|
|
|
sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML(
|
|
R"html(<body><p><strike id="strike">old</strike></p></body>)html" ) );
|
|
SceneManager::instance()->update();
|
|
|
|
auto* strike = sceneNode->getRoot()->find( "strike" )->asType<UITextSpan>();
|
|
ASSERT_TRUE( strike != nullptr );
|
|
EXPECT_TRUE( 0 != ( strike->getTextDecoration() & Text::StrikeThrough ) );
|
|
|
|
Engine::destroySingleton();
|
|
}
|
|
|
|
UTEST( UIHTML, FontSizeAbsoluteKeywordsUseBrowserScale ) {
|
|
init_ui_test();
|
|
UISceneNode* sceneNode = SceneManager::instance()->getUISceneNode();
|
|
|
|
sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( R"html(
|
|
<body>
|
|
<div id="xsmall" style="font-size: x-small">x-small</div>
|
|
<div id="small" style="font-size: small">small</div>
|
|
<div id="medium" style="font-size: medium">medium</div>
|
|
<div id="large" style="font-size: large">large</div>
|
|
<div id="parent" style="font-size: 20px">
|
|
<span id="smaller" style="font-size: smaller">smaller</span>
|
|
<span id="larger" style="font-size: larger">larger</span>
|
|
</div>
|
|
</body>
|
|
)html" ) );
|
|
SceneManager::instance()->update();
|
|
|
|
auto fontSize = [sceneNode]( const std::string& id ) {
|
|
auto* node = sceneNode->getRoot()->find( id );
|
|
return node->isType( UI_TYPE_TEXTSPAN ) ? node->asType<UITextSpan>()->getFontSize()
|
|
: node->asType<UIRichText>()->getFontSize();
|
|
};
|
|
|
|
EXPECT_EQ( fontSize( "xsmall" ), 10u );
|
|
EXPECT_EQ( fontSize( "small" ), 13u );
|
|
EXPECT_EQ( fontSize( "medium" ), 16u );
|
|
EXPECT_EQ( fontSize( "large" ), 18u );
|
|
EXPECT_EQ( fontSize( "smaller" ), 17u );
|
|
EXPECT_EQ( fontSize( "larger" ), 24u );
|
|
|
|
Engine::destroySingleton();
|
|
}
|
|
|
|
UTEST( UIHTML, WhiteSpaceNowrapDisablesSoftWrap ) {
|
|
init_ui_test();
|
|
UISceneNode* sceneNode = SceneManager::instance()->getUISceneNode();
|
|
|
|
const std::string text = "alpha beta gamma delta epsilon zeta eta theta iota kappa lambda mu";
|
|
|
|
sceneNode->loadLayoutFromString(
|
|
HTMLFormatter::HTMLtoXML( String::format( R"html(
|
|
<body>
|
|
<div id="wrap" style="width: 120px; font-size: 16px">%s</div>
|
|
<div id="nowrap" style="width: 120px; font-size: 16px; white-space: nowrap">
|
|
%s
|
|
</div>
|
|
</body>
|
|
)html",
|
|
text.c_str(), text.c_str() ) ) );
|
|
SceneManager::instance()->update();
|
|
|
|
auto* wrap = sceneNode->getRoot()->find( "wrap" )->asType<UIRichText>();
|
|
auto* nowrap = sceneNode->getRoot()->find( "nowrap" )->asType<UIRichText>();
|
|
ASSERT_TRUE( wrap != nullptr );
|
|
ASSERT_TRUE( nowrap != nullptr );
|
|
|
|
EXPECT_GT( wrap->getRichTextPtr()->getLines().size(), (size_t)1 );
|
|
EXPECT_EQ( nowrap->getRichTextPtr()->getLines().size(), (size_t)1 );
|
|
EXPECT_FALSE( nowrap->getLineWrap() );
|
|
|
|
Engine::destroySingleton();
|
|
}
|
|
|
|
UTEST( UIHTML, WhiteSpaceNowrapKeepsInlineListOnOneLine ) {
|
|
init_ui_test();
|
|
UISceneNode* sceneNode = SceneManager::instance()->getUISceneNode();
|
|
|
|
sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( R"html(
|
|
<body>
|
|
<div id="bar" style="width: 120px; white-space: nowrap; overflow: hidden">
|
|
<ul id="list" style="display: inline; list-style: none; margin: 0; padding: 0">
|
|
<li style="display: inline; white-space: nowrap">alpha</li>
|
|
<li style="display: inline; white-space: nowrap">beta</li>
|
|
<li style="display: inline; white-space: nowrap">gamma</li>
|
|
<li style="display: inline; white-space: nowrap">delta</li>
|
|
<li style="display: inline; white-space: nowrap">epsilon</li>
|
|
<li style="display: inline; white-space: nowrap">zeta</li>
|
|
<li style="display: inline; white-space: nowrap">eta</li>
|
|
<li style="display: inline; white-space: nowrap">theta</li>
|
|
</ul>
|
|
</div>
|
|
</body>
|
|
)html" ) );
|
|
SceneManager::instance()->update();
|
|
|
|
auto* bar = sceneNode->getRoot()->find( "bar" )->asType<UIRichText>();
|
|
auto* list = sceneNode->getRoot()->find( "list" )->asType<UIRichText>();
|
|
ASSERT_TRUE( bar != nullptr );
|
|
ASSERT_TRUE( list != nullptr );
|
|
|
|
EXPECT_EQ( bar->getRichTextPtr()->getLines().size(), (size_t)1 );
|
|
EXPECT_FALSE( bar->getLineWrap() );
|
|
EXPECT_EQ( list->getRichTextPtr()->getLines().size(), (size_t)1 );
|
|
EXPECT_FALSE( list->getLineWrap() );
|
|
|
|
Engine::destroySingleton();
|
|
}
|
|
|
|
UTEST( UIHTML, WhiteSpaceNowrapContinuesInlineContentAfterOverflow ) {
|
|
init_ui_test();
|
|
UISceneNode* sceneNode = SceneManager::instance()->getUISceneNode();
|
|
|
|
sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( R"html(
|
|
<body>
|
|
<div id="bar" style="width: 140px; white-space: nowrap; overflow: hidden">
|
|
<ul id="first" style="display: inline; list-style: none; margin: 0; padding: 0">
|
|
<li style="display: inline">home</li>
|
|
<li style="display: inline"> - popular</li>
|
|
<li style="display: inline"> - all</li>
|
|
<li style="display: inline"> - friends</li>
|
|
</ul>
|
|
<span>|</span>
|
|
<ul id="second" style="display: inline; list-style: none; margin: 0; padding: 0">
|
|
<li style="display: inline">movies</li>
|
|
<li style="display: inline"> - videos</li>
|
|
<li style="display: inline"> - pcgaming</li>
|
|
<li style="display: inline"> - gamedev</li>
|
|
<li style="display: inline"> - science</li>
|
|
<li style="display: inline"> - space</li>
|
|
</ul>
|
|
</div>
|
|
</body>
|
|
)html" ) );
|
|
SceneManager::instance()->update();
|
|
|
|
auto* bar = sceneNode->getRoot()->find( "bar" )->asType<UIRichText>();
|
|
auto* first = sceneNode->getRoot()->find( "first" )->asType<UIRichText>();
|
|
auto* second = sceneNode->getRoot()->find( "second" )->asType<UIRichText>();
|
|
ASSERT_TRUE( bar != nullptr );
|
|
ASSERT_TRUE( first != nullptr );
|
|
ASSERT_TRUE( second != nullptr );
|
|
|
|
EXPECT_FALSE( bar->getLineWrap() );
|
|
EXPECT_FALSE( first->getLineWrap() );
|
|
EXPECT_FALSE( second->getLineWrap() );
|
|
EXPECT_EQ( bar->getRichTextPtr()->getLines().size(), (size_t)1 );
|
|
EXPECT_EQ( first->getRichTextPtr()->getLines().size(), (size_t)1 );
|
|
EXPECT_EQ( second->getRichTextPtr()->getLines().size(), (size_t)1 );
|
|
EXPECT_NEAR( first->convertToWorldSpace( { 0, 0 } ).y,
|
|
second->convertToWorldSpace( { 0, 0 } ).y, 1.f );
|
|
|
|
Engine::destroySingleton();
|
|
}
|
|
|
|
UTEST( UIHTML, WhiteSpaceCollapsePreCodePreservesIndentation ) {
|
|
init_ui_test();
|
|
UISceneNode* sceneNode = SceneManager::instance()->getUISceneNode();
|
|
|
|
sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( R"html(
|
|
<body>
|
|
<pre id="pre"><code id="code">if (x) {
|
|
return 1;
|
|
}</code></pre>
|
|
</body>
|
|
)html" ) );
|
|
SceneManager::instance()->update();
|
|
|
|
auto* pre = sceneNode->getRoot()->find( "pre" )->asType<UIRichText>();
|
|
auto* code = sceneNode->getRoot()->find( "code" );
|
|
ASSERT_TRUE( pre != nullptr );
|
|
ASSERT_TRUE( code != nullptr );
|
|
|
|
EXPECT_FALSE( code->isType( UI_TYPE_CODEEDITOR ) );
|
|
EXPECT_GE( pre->getRichTextPtr()->getLines().size(), (size_t)3 );
|
|
EXPECT_STRINGEQ( uiHtmlLineText( *pre->getRichTextPtr(), 0 ), "if (x) {" );
|
|
EXPECT_STRINGEQ( uiHtmlLineText( *pre->getRichTextPtr(), 1 ), " return 1;" );
|
|
EXPECT_STRINGEQ( uiHtmlLineText( *pre->getRichTextPtr(), 2 ), "}" );
|
|
|
|
Engine::destroySingleton();
|
|
}
|
|
|
|
UTEST( UIHTML, PreCodeSimpleFixtureKeepsCompactCodeLines ) {
|
|
init_ui_test();
|
|
UISceneNode* sceneNode = SceneManager::instance()->getUISceneNode();
|
|
sceneNode->setURI( "file://" + Sys::getProcessPath() + "assets/html/" );
|
|
|
|
std::string html;
|
|
ASSERT_TRUE( FileSystem::fileGet( "assets/html/pre.code.html", html ) );
|
|
sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( html ) );
|
|
SceneManager::instance()->update();
|
|
|
|
UIRichText* pre = nullptr;
|
|
Node* code = nullptr;
|
|
sceneNode->getRoot()->forEachNode( [&pre, &code]( Node* node ) {
|
|
if ( node->isWidget() && node->asType<UIWidget>()->getElementTag() == "pre" )
|
|
pre = node->asType<UIRichText>();
|
|
if ( node->isWidget() && node->asType<UIWidget>()->getElementTag() == "code" )
|
|
code = node;
|
|
} );
|
|
ASSERT_TRUE( pre != nullptr );
|
|
ASSERT_TRUE( code != nullptr );
|
|
EXPECT_FALSE( code->isType( UI_TYPE_CODEEDITOR ) );
|
|
auto* codeSpan = code->asType<UITextSpan>();
|
|
ASSERT_TRUE( codeSpan != nullptr );
|
|
|
|
const auto& lines = pre->getRichTextPtr()->getLines();
|
|
ASSERT_GE( lines.size(), (size_t)3 );
|
|
ASSERT_LE( lines.size(), (size_t)4 );
|
|
EXPECT_STRINGEQ( uiHtmlLineText( *pre->getRichTextPtr(), 0 ), "void main() {" );
|
|
EXPECT_STRINGEQ( uiHtmlLineText( *pre->getRichTextPtr(), 1 ), " printf(\"Hello World\");" );
|
|
EXPECT_STRINGEQ( uiHtmlLineText( *pre->getRichTextPtr(), 2 ), "}" );
|
|
if ( lines.size() == 4 )
|
|
EXPECT_STRINGEQ( uiHtmlLineText( *pre->getRichTextPtr(), 3 ), "" );
|
|
ASSERT_FALSE( lines[1].spans.empty() );
|
|
ASSERT_TRUE( lines[1].spans[0].text != nullptr );
|
|
ASSERT_FALSE( lines[1].spans[0].text->getString().empty() );
|
|
EXPECT_TRUE( lines[1].spans[0].text->getString()[0] != '\n' );
|
|
ASSERT_FALSE( lines[2].spans.empty() );
|
|
ASSERT_TRUE( lines[2].spans[0].text != nullptr );
|
|
ASSERT_FALSE( lines[2].spans[0].text->getString().empty() );
|
|
EXPECT_TRUE( lines[2].spans[0].text->getString()[0] != '\n' );
|
|
|
|
ASSERT_EQ( codeSpan->getHitBoxes().size(), (size_t)3 );
|
|
Float expectedLineHeight = pre->getLineHeightPx();
|
|
if ( expectedLineHeight <= 0.f ) {
|
|
for ( const auto& span : lines[0].spans ) {
|
|
if ( span.type == RichText::RenderSpan::Type::Text && span.text &&
|
|
!span.text->getString().empty() ) {
|
|
const auto& fontStyle = span.text->getFontStyleConfig();
|
|
ASSERT_TRUE( fontStyle.Font != nullptr );
|
|
expectedLineHeight = fontStyle.Font->getLineSpacing( fontStyle.CharacterSize );
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
ASSERT_GT( expectedLineHeight, 0.f );
|
|
Float compactLineLimit = expectedLineHeight * 1.25f;
|
|
for ( size_t i = 1; i < 3; ++i ) {
|
|
Float lineDelta = lines[i].y - lines[i - 1].y;
|
|
EXPECT_LE( lineDelta, compactLineLimit );
|
|
EXPECT_LE( lines[i].height, compactLineLimit );
|
|
|
|
const Rectf& prev = codeSpan->getHitBoxes()[i - 1];
|
|
const Rectf& cur = codeSpan->getHitBoxes()[i];
|
|
EXPECT_NEAR( cur.Top - prev.Top, lineDelta, 1.f );
|
|
EXPECT_LE( cur.Top - prev.Top, compactLineLimit );
|
|
}
|
|
|
|
Engine::destroySingleton();
|
|
}
|
|
|
|
UTEST( UIHTML, PreCodeFixtureDiscardsFinalNewlineBeforeEndTag ) {
|
|
init_ui_test();
|
|
UISceneNode* sceneNode = SceneManager::instance()->getUISceneNode();
|
|
sceneNode->setURI( "file://" + Sys::getProcessPath() + "assets/html/" );
|
|
|
|
std::string html;
|
|
ASSERT_TRUE( FileSystem::fileGet( "assets/html/pre.code.2.html", html ) );
|
|
sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( html ) );
|
|
SceneManager::instance()->update();
|
|
|
|
UIRichText* pre = nullptr;
|
|
Node* code = nullptr;
|
|
sceneNode->getRoot()->forEachNode( [&pre, &code]( Node* node ) {
|
|
if ( node->isWidget() && node->asType<UIWidget>()->getElementTag() == "pre" )
|
|
pre = node->asType<UIRichText>();
|
|
if ( node->isWidget() && node->asType<UIWidget>()->getElementTag() == "code" )
|
|
code = node;
|
|
} );
|
|
ASSERT_TRUE( pre != nullptr );
|
|
ASSERT_TRUE( code != nullptr );
|
|
EXPECT_FALSE( code->isType( UI_TYPE_CODEEDITOR ) );
|
|
|
|
const auto& lines = pre->getRichTextPtr()->getLines();
|
|
ASSERT_EQ( lines.size(), (size_t)3 );
|
|
EXPECT_STRINGEQ( uiHtmlLineText( *pre->getRichTextPtr(), 0 ), "int x = 42;" );
|
|
EXPECT_STRINGEQ( uiHtmlLineText( *pre->getRichTextPtr(), 1 ), "const char* y = \"hello\";" );
|
|
EXPECT_STRINGEQ( uiHtmlLineText( *pre->getRichTextPtr(), 2 ), "int z = foo(x, y);" );
|
|
|
|
Engine::destroySingleton();
|
|
}
|
|
|
|
UTEST( UIHTML, PreCodeUsesCodeEditorOnlyForMarkdownAncestorOrGlobalOptIn ) {
|
|
init_ui_test();
|
|
UISceneNode* sceneNode = SceneManager::instance()->getUISceneNode();
|
|
|
|
sceneNode->loadLayoutFromString( R"xml(
|
|
<markdownview id="md">
|
|
<pre id="md_pre"><code id="md_code" class="language-cpp">void main() {
|
|
printf("Hello World");
|
|
}</code></pre>
|
|
</markdownview>
|
|
)xml" );
|
|
SceneManager::instance()->update();
|
|
|
|
auto* markdownCode = sceneNode->getRoot()->find( "md_code" );
|
|
ASSERT_TRUE( markdownCode != nullptr );
|
|
ASSERT_TRUE( markdownCode->isType( UI_TYPE_CODEEDITOR ) );
|
|
EXPECT_TRUE( markdownCode->asType<UICodeEditor>()->getDocument().getText().find( "printf" ) !=
|
|
String::InvalidPos );
|
|
|
|
Engine::destroySingleton();
|
|
|
|
init_ui_test();
|
|
sceneNode = SceneManager::instance()->getUISceneNode();
|
|
UIRichText::setUseCodeEditorForPreCodeBlocks( true );
|
|
sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( R"html(
|
|
<body>
|
|
<pre id="pre"><code id="code">int value = 1;</code></pre>
|
|
</body>
|
|
)html" ) );
|
|
SceneManager::instance()->update();
|
|
|
|
auto* optInCode = sceneNode->getRoot()->find( "code" );
|
|
ASSERT_TRUE( optInCode != nullptr );
|
|
EXPECT_TRUE( optInCode->isType( UI_TYPE_CODEEDITOR ) );
|
|
UIRichText::setUseCodeEditorForPreCodeBlocks( false );
|
|
|
|
Engine::destroySingleton();
|
|
}
|
|
|
|
UTEST( UIHTML, PreCodeBlockFixtureKeepsCompactCodeLines ) {
|
|
init_ui_test();
|
|
UISceneNode* sceneNode = SceneManager::instance()->getUISceneNode();
|
|
sceneNode->setURI( "file://" + Sys::getProcessPath() + "assets/html/" );
|
|
|
|
std::string html;
|
|
ASSERT_TRUE( FileSystem::fileGet( "assets/html/pre_code_block.html", html ) );
|
|
sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( html ) );
|
|
SceneManager::instance()->update();
|
|
|
|
UIRichText* pre = nullptr;
|
|
sceneNode->getRoot()->forEachNode( [&pre]( Node* node ) {
|
|
if ( pre == nullptr && node->isWidget() &&
|
|
node->asType<UIWidget>()->getElementTag() == "pre" )
|
|
pre = node->asType<UIRichText>();
|
|
} );
|
|
ASSERT_TRUE( pre != nullptr );
|
|
|
|
const auto& lines = pre->getRichTextPtr()->getLines();
|
|
ASSERT_EQ( lines.size(), (size_t)19 );
|
|
EXPECT_STRINGEQ( uiHtmlLineText( *pre->getRichTextPtr(), 0 ), "let stepper () =" );
|
|
EXPECT_STRINGEQ( uiHtmlLineText( *pre->getRichTextPtr(), 1 ),
|
|
" // Execute a single instruction" );
|
|
EXPECT_STRINGEQ( uiHtmlLineText( *pre->getRichTextPtr(), 18 ), " mCycles " );
|
|
|
|
Float expectedLineHeight = pre->getLineHeightPx();
|
|
ASSERT_GT( expectedLineHeight, 0.f );
|
|
Float compactLineLimit = expectedLineHeight * 1.25f;
|
|
for ( size_t i = 1; i < lines.size(); ++i ) {
|
|
EXPECT_LE( lines[i].y - lines[i - 1].y, compactLineLimit );
|
|
EXPECT_LE( lines[i].height, compactLineLimit );
|
|
}
|
|
|
|
std::vector<UITextSpan*> codeLineSpans;
|
|
for ( size_t i = 1; i <= 19; ++i ) {
|
|
auto* lineSpan = sceneNode->getRoot()->find<UITextSpan>( String::format( "cb1-%zu", i ) );
|
|
ASSERT_TRUE( lineSpan != nullptr );
|
|
ASSERT_EQ( lineSpan->getHitBoxes().size(), (size_t)1 );
|
|
EXPECT_LE( lineSpan->getHitBoxes()[0].getHeight(), compactLineLimit );
|
|
codeLineSpans.push_back( lineSpan );
|
|
}
|
|
for ( size_t i = 1; i < codeLineSpans.size(); ++i ) {
|
|
const Rectf& prev = codeLineSpans[i - 1]->getHitBoxes()[0];
|
|
const Rectf& cur = codeLineSpans[i]->getHitBoxes()[0];
|
|
Float prevTop = codeLineSpans[i - 1]->getPixelsPosition().y + prev.Top;
|
|
Float curTop = codeLineSpans[i]->getPixelsPosition().y + cur.Top;
|
|
EXPECT_NEAR( curTop - prevTop, lines[i].y - lines[i - 1].y, 1.f );
|
|
EXPECT_LE( curTop - prevTop, compactLineLimit );
|
|
}
|
|
|
|
Engine::destroySingleton();
|
|
}
|
|
|
|
UTEST( UIHTML, WhiteSpacePreInheritedBySyntaxSpansPreservesEmbeddedBreaks ) {
|
|
init_ui_test();
|
|
UISceneNode* sceneNode = SceneManager::instance()->getUISceneNode();
|
|
sceneNode->setURI( "file://" + Sys::getProcessPath() + "assets/html/" );
|
|
|
|
std::string html;
|
|
ASSERT_TRUE( FileSystem::fileGet( "assets/html/whitespace_pre.html", html ) );
|
|
sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( html ) );
|
|
SceneManager::instance()->update();
|
|
|
|
UIRichText* pre = nullptr;
|
|
sceneNode->getRoot()->forEachNode( [&pre]( Node* node ) {
|
|
if ( pre == nullptr && node->isWidget() &&
|
|
node->asType<UIWidget>()->getElementTag() == "pre" )
|
|
pre = node->asType<UIRichText>();
|
|
} );
|
|
ASSERT_TRUE( pre != nullptr );
|
|
|
|
const auto& lines = pre->getRichTextPtr()->getLines();
|
|
ASSERT_EQ( lines.size(), (size_t)11 );
|
|
EXPECT_STRINGEQ( uiHtmlLineText( *pre->getRichTextPtr(), 0 ),
|
|
"// in the \"LSP Settings\" file, under \"clients[]\"" );
|
|
EXPECT_STRINGEQ( uiHtmlLineText( *pre->getRichTextPtr(), 1 ), "\"md-lsp\": {" );
|
|
EXPECT_STRINGEQ( uiHtmlLineText( *pre->getRichTextPtr(), 3 ), " \"enabled\": true," );
|
|
EXPECT_STRINGEQ( uiHtmlLineText( *pre->getRichTextPtr(), 10 ), "}," );
|
|
|
|
Engine::destroySingleton();
|
|
}
|
|
|
|
UTEST( UIHTML, WhiteSpaceCollapsePreLinePreservesBreaksOnly ) {
|
|
init_ui_test();
|
|
UISceneNode* sceneNode = SceneManager::instance()->getUISceneNode();
|
|
|
|
sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( R"html(
|
|
<body>
|
|
<div id="preline" style="white-space: pre-line">alpha beta
|
|
gamma</div>
|
|
</body>
|
|
)html" ) );
|
|
SceneManager::instance()->update();
|
|
|
|
auto* preline = sceneNode->getRoot()->find( "preline" )->asType<UIRichText>();
|
|
ASSERT_TRUE( preline != nullptr );
|
|
|
|
EXPECT_EQ( preline->getRichTextPtr()->getLines().size(), (size_t)2 );
|
|
EXPECT_STRINGEQ( uiHtmlLineText( *preline->getRichTextPtr(), 0 ), "alpha beta" );
|
|
EXPECT_STRINGEQ( uiHtmlLineText( *preline->getRichTextPtr(), 1 ), " gamma" );
|
|
|
|
Engine::destroySingleton();
|
|
}
|
|
|
|
UTEST( UIHTML, WhiteSpaceCollapseBreakSpacesAffectsIntrinsicWidth ) {
|
|
init_ui_test();
|
|
UISceneNode* sceneNode = SceneManager::instance()->getUISceneNode();
|
|
|
|
sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( R"html(
|
|
<body>
|
|
<div id="normal" style="white-space: pre-wrap; font-size: 16px"> </div>
|
|
<div id="breakspaces" style="white-space: break-spaces; font-size: 16px"> </div>
|
|
</body>
|
|
)html" ) );
|
|
SceneManager::instance()->update();
|
|
|
|
auto* normal = sceneNode->getRoot()->find( "normal" )->asType<UIRichText>();
|
|
auto* breakSpaces = sceneNode->getRoot()->find( "breakspaces" )->asType<UIRichText>();
|
|
ASSERT_TRUE( normal != nullptr );
|
|
ASSERT_TRUE( breakSpaces != nullptr );
|
|
|
|
EXPECT_LT( normal->getMinIntrinsicWidth(), breakSpaces->getMinIntrinsicWidth() );
|
|
EXPECT_GT( breakSpaces->getMinIntrinsicWidth(), 0.f );
|
|
EXPECT_STRINGEQ( uiHtmlRenderedText( *breakSpaces->getRichTextPtr() ), " " );
|
|
|
|
Engine::destroySingleton();
|
|
}
|
|
|
|
UTEST( UIHTML, WhiteSpaceCollapsePreservedTabsUseTabSize ) {
|
|
init_ui_test();
|
|
UISceneNode* sceneNode = SceneManager::instance()->getUISceneNode();
|
|
|
|
sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( R"html(
|
|
<body>
|
|
<pre id="tab2" style="font-family: monospace; font-size: 16px; tab-size: 2">a b</pre>
|
|
<pre id="tab8" style="font-family: monospace; font-size: 16px; tab-size: 8">a b</pre>
|
|
<pre id="tabpx" style="font-family: monospace; font-size: 16px; tab-size: 32px">a b</pre>
|
|
</body>
|
|
)html" ) );
|
|
SceneManager::instance()->update();
|
|
|
|
auto* tab2 = sceneNode->getRoot()->find( "tab2" )->asType<UIRichText>();
|
|
auto* tab8 = sceneNode->getRoot()->find( "tab8" )->asType<UIRichText>();
|
|
auto* tabpx = sceneNode->getRoot()->find( "tabpx" )->asType<UIRichText>();
|
|
ASSERT_TRUE( tab2 != nullptr );
|
|
ASSERT_TRUE( tab8 != nullptr );
|
|
ASSERT_TRUE( tabpx != nullptr );
|
|
|
|
ASSERT_EQ( tab2->getRichTextPtr()->getLines().size(), (size_t)1 );
|
|
ASSERT_EQ( tab8->getRichTextPtr()->getLines().size(), (size_t)1 );
|
|
ASSERT_EQ( tabpx->getRichTextPtr()->getLines().size(), (size_t)1 );
|
|
EXPECT_STRINGEQ( uiHtmlLineText( *tab2->getRichTextPtr(), 0 ), "a\tb" );
|
|
EXPECT_STRINGEQ( uiHtmlLineText( *tab8->getRichTextPtr(), 0 ), "a\tb" );
|
|
EXPECT_STRINGEQ( uiHtmlLineText( *tabpx->getRichTextPtr(), 0 ), "a\tb" );
|
|
EXPECT_EQ( tab2->getTabSize(), 2u );
|
|
EXPECT_EQ( tab8->getTabSize(), 8u );
|
|
EXPECT_GT( tabpx->getTabSize(), 2u );
|
|
|
|
const Float tab2Width = tab2->getRichTextPtr()->getLines()[0].width;
|
|
const Float tab8Width = tab8->getRichTextPtr()->getLines()[0].width;
|
|
const Float tabpxWidth = tabpx->getRichTextPtr()->getLines()[0].width;
|
|
EXPECT_GT( tab8Width, tab2Width );
|
|
EXPECT_GT( tabpxWidth, tab2Width );
|
|
|
|
ASSERT_FALSE( tab2->getRichTextPtr()->getLines()[0].spans.empty() );
|
|
ASSERT_FALSE( tab8->getRichTextPtr()->getLines()[0].spans.empty() );
|
|
ASSERT_FALSE( tabpx->getRichTextPtr()->getLines()[0].spans.empty() );
|
|
ASSERT_TRUE( tab2->getRichTextPtr()->getLines()[0].spans[0].text != nullptr );
|
|
ASSERT_TRUE( tab8->getRichTextPtr()->getLines()[0].spans[0].text != nullptr );
|
|
ASSERT_TRUE( tabpx->getRichTextPtr()->getLines()[0].spans[0].text != nullptr );
|
|
EXPECT_EQ( tab2->getRichTextPtr()->getLines()[0].spans[0].text->getTabWidth(), 2u );
|
|
EXPECT_EQ( tab8->getRichTextPtr()->getLines()[0].spans[0].text->getTabWidth(), 8u );
|
|
EXPECT_EQ( tabpx->getRichTextPtr()->getLines()[0].spans[0].text->getTabWidth(),
|
|
tabpx->getTabSize() );
|
|
|
|
Engine::destroySingleton();
|
|
}
|
|
|
|
UTEST( UIRichText, anchorMargins ) {
|
|
auto win = Engine::instance()->createWindow(
|
|
WindowSettings( 800, 600, "Anchor Margins Test", WindowStyle::Default,
|
|
WindowBackend::Default, 32, {}, 1, false, true ),
|
|
ContextSettings( false, 0, 0, GLv_default, true, false ) );
|
|
FileSystem::changeWorkingDirectory( Sys::getProcessPath() );
|
|
|
|
FontTrueType* font = FontTrueType::New( "NotoSans-Regular" ).get();
|
|
font->loadFromFile( "../assets/fonts/NotoSans-Regular.ttf" );
|
|
ASSERT_TRUE( font != nullptr && font->loaded() );
|
|
FontFamily::loadFromRegular( font );
|
|
|
|
UISceneNode* sceneNode = UISceneNode::New();
|
|
SceneManager::instance()->add( sceneNode );
|
|
UI::UIThemeManager* themeManager = sceneNode->getUIThemeManager();
|
|
themeManager->setDefaultFont( font );
|
|
sceneNode->setURI( "file://" + Sys::getProcessPath() + "assets/html/" );
|
|
std::string html;
|
|
FileSystem::fileGet( "assets/html/anchor_margins.html", html );
|
|
sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( html ) );
|
|
win->setClearColor( Color::White );
|
|
|
|
win->getInput()->update();
|
|
SceneManager::instance()->update();
|
|
|
|
win->clear();
|
|
SceneManager::instance()->draw();
|
|
win->display();
|
|
|
|
auto anchors = sceneNode->getRoot()->findAllByTag( "a" );
|
|
|
|
for ( auto anchor : anchors ) {
|
|
auto a = anchor->asType<UIAnchorSpan>();
|
|
EXPECT_EQ( anchor->getPixelsSize().getHeight(),
|
|
a->getFont()->getLineSpacing( a->getFontSize() ) );
|
|
}
|
|
|
|
compareImages( utest_state, utest_result, win, "eepp-ui-anchor-margins", "html" );
|
|
|
|
Engine::destroySingleton();
|
|
}
|
|
|
|
UTEST( UIRichText, spanPadding ) {
|
|
auto win = Engine::instance()->createWindow(
|
|
WindowSettings( 800, 600, "Span Padding Test", WindowStyle::Default, WindowBackend::Default,
|
|
32, {}, 1, false, true ),
|
|
ContextSettings( false, 0, 0, GLv_default, true, false ) );
|
|
FileSystem::changeWorkingDirectory( Sys::getProcessPath() );
|
|
|
|
FontTrueType* font = FontTrueType::New( "NotoSans-Regular" ).get();
|
|
font->loadFromFile( "../assets/fonts/NotoSans-Regular.ttf" );
|
|
ASSERT_TRUE( font != nullptr && font->loaded() );
|
|
FontFamily::loadFromRegular( font );
|
|
|
|
UISceneNode* sceneNode = UISceneNode::New();
|
|
SceneManager::instance()->add( sceneNode );
|
|
UI::UIThemeManager* themeManager = sceneNode->getUIThemeManager();
|
|
themeManager->setDefaultFont( font );
|
|
sceneNode->setURI( "file://" + Sys::getProcessPath() + "assets/html/" );
|
|
std::string html;
|
|
FileSystem::fileGet( "assets/html/span_padding.html", html );
|
|
sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( html ) );
|
|
win->setClearColor( Color::White );
|
|
|
|
win->getInput()->update();
|
|
SceneManager::instance()->update();
|
|
|
|
win->clear();
|
|
SceneManager::instance()->draw();
|
|
win->display();
|
|
|
|
compareImages( utest_state, utest_result, win, "eepp-ui-span-padding", "html" );
|
|
|
|
Engine::destroySingleton();
|
|
}
|
|
|
|
UTEST( UIRichText, anchorPadding ) {
|
|
auto win = Engine::instance()->createWindow(
|
|
WindowSettings( 800, 600, "Anchor Span Padding Test", WindowStyle::Default,
|
|
WindowBackend::Default, 32, {}, 1, false, true ),
|
|
ContextSettings( false, 0, 0, GLv_default, true, false ) );
|
|
FileSystem::changeWorkingDirectory( Sys::getProcessPath() );
|
|
|
|
FontTrueType* font = FontTrueType::New( "NotoSans-Regular" ).get();
|
|
font->loadFromFile( "../assets/fonts/NotoSans-Regular.ttf" );
|
|
ASSERT_TRUE( font != nullptr && font->loaded() );
|
|
FontFamily::loadFromRegular( font );
|
|
|
|
UISceneNode* sceneNode = UISceneNode::New();
|
|
SceneManager::instance()->add( sceneNode );
|
|
UI::UIThemeManager* themeManager = sceneNode->getUIThemeManager();
|
|
themeManager->setDefaultFont( font );
|
|
sceneNode->setURI( "file://" + Sys::getProcessPath() + "assets/html/" );
|
|
std::string html;
|
|
FileSystem::fileGet( "assets/html/anchor_padding.html", html );
|
|
sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( html ) );
|
|
win->setClearColor( Color::White );
|
|
|
|
win->getInput()->update();
|
|
SceneManager::instance()->update();
|
|
|
|
win->clear();
|
|
SceneManager::instance()->draw();
|
|
win->display();
|
|
|
|
compareImages( utest_state, utest_result, win, "eepp-ui-anchor-padding", "html" );
|
|
|
|
auto anchors = sceneNode->getRoot()->findAllByTag( "a" );
|
|
ASSERT_GE( anchors.size(), (size_t)1 );
|
|
auto downloadLink = anchors[0]->asType<UIWidget>();
|
|
EXPECT_NEAR( downloadLink->getPixelsSize().getWidth(), 81.f, 3.f );
|
|
EXPECT_NEAR( downloadLink->getPixelsSize().getHeight(), 28.f, 3.f );
|
|
|
|
Engine::destroySingleton();
|
|
}
|
|
|
|
UTEST( UIRichText, anchorPaddingLineHeight ) {
|
|
auto win = Engine::instance()->createWindow(
|
|
WindowSettings( 800, 600, "Anchor Padding LineHeight Test", WindowStyle::Default,
|
|
WindowBackend::Default, 32, {}, 1, false, true ),
|
|
ContextSettings( false, 0, 0, GLv_default, true, false ) );
|
|
FileSystem::changeWorkingDirectory( Sys::getProcessPath() );
|
|
|
|
FontTrueType* font = FontTrueType::New( "NotoSans-Regular" ).get();
|
|
font->loadFromFile( "../assets/fonts/NotoSans-Regular.ttf" );
|
|
ASSERT_TRUE( font != nullptr && font->loaded() );
|
|
FontFamily::loadFromRegular( font );
|
|
|
|
UISceneNode* sceneNode = UISceneNode::New();
|
|
SceneManager::instance()->add( sceneNode );
|
|
UI::UIThemeManager* themeManager = sceneNode->getUIThemeManager();
|
|
themeManager->setDefaultFont( font );
|
|
sceneNode->setURI( "file://" + Sys::getProcessPath() + "assets/html/" );
|
|
std::string html;
|
|
FileSystem::fileGet( "assets/html/anchor_padding_lineheight.html", html );
|
|
sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( html ) );
|
|
win->setClearColor( Color::White );
|
|
|
|
win->getInput()->update();
|
|
SceneManager::instance()->update();
|
|
|
|
win->clear();
|
|
SceneManager::instance()->draw();
|
|
win->display();
|
|
|
|
compareImages( utest_state, utest_result, win, "eepp-ui-anchor-padding-lineheight", "html" );
|
|
|
|
auto anchors = sceneNode->getRoot()->findAllByTag( "a" );
|
|
ASSERT_GE( anchors.size(), (size_t)1 );
|
|
auto downloadLink = anchors[0]->asType<UIWidget>();
|
|
EXPECT_NEAR( downloadLink->getPixelsSize().getWidth(), 81.f, 3.f );
|
|
EXPECT_NEAR( downloadLink->getPixelsSize().getHeight(), 28.f, 3.f );
|
|
|
|
Engine::destroySingleton();
|
|
}
|
|
|
|
UTEST( UIHTML, InlineBaselineAlignmentProperties ) {
|
|
Engine::instance()->createWindow( WindowSettings( 800, 600, "Inline Baseline Alignment Test",
|
|
WindowStyle::Default, WindowBackend::Default,
|
|
32, {}, 1, false, true ),
|
|
ContextSettings( false, 0, 0, GLv_default, true, false ) );
|
|
FileSystem::changeWorkingDirectory( Sys::getProcessPath() );
|
|
|
|
FontTrueType* font = FontTrueType::New( "NotoSans-Regular" ).get();
|
|
font->loadFromFile( "../assets/fonts/NotoSans-Regular.ttf" );
|
|
ASSERT_TRUE( font != nullptr && font->loaded() );
|
|
FontFamily::loadFromRegular( font );
|
|
|
|
UISceneNode* sceneNode = UISceneNode::New();
|
|
SceneManager::instance()->add( sceneNode );
|
|
sceneNode->getUIThemeManager()->setDefaultFont( font );
|
|
sceneNode->loadLayoutFromString( R"html(
|
|
<vbox layout_width="wrap_content" layout_height="wrap_content">
|
|
<RichText id="baseline" font-size="20dp" layout_width="wrap_content" layout_height="wrap_content">
|
|
A<span id="base_box" display="inline-block" layout_width="20dp" layout_height="20dp" vertical-align="baseline">X</span>
|
|
</RichText>
|
|
<RichText id="middle" font-size="20dp" layout_width="wrap_content" layout_height="wrap_content">
|
|
A<span id="middle_box" display="inline-block" layout_width="20dp" layout_height="20dp" vertical-align="middle"><span id="middle_child">X</span></span>
|
|
</RichText>
|
|
<RichText id="alignment_middle" font-size="20dp" layout_width="wrap_content" layout_height="wrap_content">
|
|
A<span id="alignment_box" display="inline-block" layout_width="20dp" layout_height="20dp" alignment-baseline="middle">X</span>
|
|
</RichText>
|
|
</vbox>
|
|
)html" );
|
|
sceneNode->updateDirtyLayouts();
|
|
|
|
auto* baseline = sceneNode->getRoot()->find( "baseline" )->asType<UIRichText>();
|
|
auto* middle = sceneNode->getRoot()->find( "middle" )->asType<UIRichText>();
|
|
auto* alignmentMiddle = sceneNode->getRoot()->find( "alignment_middle" )->asType<UIRichText>();
|
|
auto* baselineBox = sceneNode->getRoot()->find( "base_box" )->asType<UIHTMLWidget>();
|
|
auto* middleBox = sceneNode->getRoot()->find( "middle_box" )->asType<UIHTMLWidget>();
|
|
auto* middleChild = sceneNode->getRoot()->find( "middle_child" )->asType<UIHTMLWidget>();
|
|
auto* alignmentBox = sceneNode->getRoot()->find( "alignment_box" )->asType<UIHTMLWidget>();
|
|
ASSERT_TRUE( baseline != nullptr );
|
|
ASSERT_TRUE( middle != nullptr );
|
|
ASSERT_TRUE( alignmentMiddle != nullptr );
|
|
ASSERT_TRUE( baselineBox != nullptr );
|
|
ASSERT_TRUE( middleBox != nullptr );
|
|
ASSERT_TRUE( middleChild != nullptr );
|
|
ASSERT_TRUE( alignmentBox != nullptr );
|
|
|
|
EXPECT_EQ( baselineBox->getBaselineAlign().type, CSSBaselineAlignment::Baseline );
|
|
EXPECT_EQ( middleBox->getBaselineAlign().type, CSSBaselineAlignment::Middle );
|
|
EXPECT_EQ( middleChild->getBaselineAlign().type, CSSBaselineAlignment::Baseline );
|
|
EXPECT_EQ( alignmentBox->getBaselineAlign().type, CSSBaselineAlignment::Middle );
|
|
|
|
Engine::destroySingleton();
|
|
}
|
|
|
|
UTEST( UIHTML, InlineBlockVerticalAlignDoesNotInflateOwnTextLine ) {
|
|
Engine::instance()->createWindow( WindowSettings( 800, 600, "Inline Block Vertical Align Test",
|
|
WindowStyle::Default, WindowBackend::Default,
|
|
32, {}, 1, false, true ),
|
|
ContextSettings( false, 0, 0, GLv_default, true, false ) );
|
|
FileSystem::changeWorkingDirectory( Sys::getProcessPath() );
|
|
|
|
FontTrueType* font = FontTrueType::New( "NotoSans-Regular" ).get();
|
|
font->loadFromFile( "../assets/fonts/NotoSans-Regular.ttf" );
|
|
ASSERT_TRUE( font != nullptr && font->loaded() );
|
|
FontFamily::loadFromRegular( font );
|
|
|
|
UISceneNode* sceneNode = UISceneNode::New();
|
|
SceneManager::instance()->add( sceneNode );
|
|
sceneNode->getUIThemeManager()->setDefaultFont( font );
|
|
sceneNode->loadLayoutFromString( R"html(
|
|
<vbox layout_width="wrap_content" layout_height="wrap_content">
|
|
<RichText id="baseline" font-size="16dp" layout_width="wrap_content" layout_height="wrap_content">
|
|
A<span id="base_box" display="inline-block" font-size="10dp" vertical-align="baseline">self.cpp</span>
|
|
</RichText>
|
|
<RichText id="middle" font-size="16dp" layout_width="wrap_content" layout_height="wrap_content">
|
|
A<span id="middle_box" display="inline-block" font-size="10dp" vertical-align="middle">self.cpp</span>
|
|
</RichText>
|
|
</vbox>
|
|
)html" );
|
|
sceneNode->updateDirtyLayouts();
|
|
|
|
auto* baseline = sceneNode->getRoot()->find( "baseline" )->asType<UIRichText>();
|
|
auto* middle = sceneNode->getRoot()->find( "middle" )->asType<UIRichText>();
|
|
auto* baselineBox = sceneNode->getRoot()->find( "base_box" )->asType<UITextSpan>();
|
|
auto* middleBox = sceneNode->getRoot()->find( "middle_box" )->asType<UITextSpan>();
|
|
ASSERT_TRUE( baseline != nullptr );
|
|
ASSERT_TRUE( middle != nullptr );
|
|
ASSERT_TRUE( baselineBox != nullptr );
|
|
ASSERT_TRUE( middleBox != nullptr );
|
|
|
|
ASSERT_EQ( baselineBox->getRichTextPtr()->getLines().size(), (size_t)1 );
|
|
ASSERT_EQ( middleBox->getRichTextPtr()->getLines().size(), (size_t)1 );
|
|
EXPECT_NEAR( middleBox->getPixelsSize().getHeight(), baselineBox->getPixelsSize().getHeight(),
|
|
0.5f );
|
|
EXPECT_NEAR( middleBox->getRichTextPtr()->getLines().front().height,
|
|
baselineBox->getRichTextPtr()->getLines().front().height, 0.5f );
|
|
EXPECT_EQ( middleBox->getBaselineAlign().type, CSSBaselineAlignment::Middle );
|
|
EXPECT_GE( middle->getRichTextPtr()->getLines().front().height,
|
|
baseline->getRichTextPtr()->getLines().front().height );
|
|
|
|
Engine::destroySingleton();
|
|
}
|
|
|
|
UTEST( UIHTMLTable, complexLayout3 ) {
|
|
auto win = Engine::instance()->createWindow(
|
|
WindowSettings( 1024, 650, "HTML Tables Test 3", VisualTestWindowStyle,
|
|
WindowBackend::Default, 32, {}, 1, false, true ),
|
|
ContextSettings( false, 0, 0, GLv_default, true, false ) );
|
|
FileSystem::changeWorkingDirectory( Sys::getProcessPath() );
|
|
|
|
FontTrueType* font = FontTrueType::New( "NotoSans-Regular" ).get();
|
|
font->loadFromFile( "../assets/fonts/NotoSans-Regular.ttf" );
|
|
ASSERT_TRUE( font != nullptr && font->loaded() );
|
|
FontFamily::loadFromRegular( font );
|
|
|
|
UISceneNode* sceneNode = UISceneNode::New();
|
|
SceneManager::instance()->add( sceneNode );
|
|
UI::UIThemeManager* themeManager = sceneNode->getUIThemeManager();
|
|
themeManager->setDefaultFont( font );
|
|
sceneNode->setURI( "file://" + Sys::getProcessPath() + "assets/html/" );
|
|
std::string html;
|
|
FileSystem::fileGet( "assets/html/hn_frontpage.html", html );
|
|
sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( html ) );
|
|
win->setClearColor( Color::White );
|
|
|
|
win->getInput()->update();
|
|
SceneManager::instance()->update();
|
|
|
|
win->clear();
|
|
SceneManager::instance()->draw();
|
|
win->display();
|
|
|
|
compareImages( utest_state, utest_result, win, "eepp-uihtmltable-complex-layout-3", "html" );
|
|
|
|
Engine::destroySingleton();
|
|
}
|
|
|
|
UTEST( UIHTMLTable, nestedPerformance ) {
|
|
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" ).get();
|
|
font->loadFromFile( "../assets/fonts/NotoSans-Regular.ttf" );
|
|
ASSERT_TRUE( font != nullptr && font->loaded() );
|
|
FontFamily::loadFromRegular( font );
|
|
|
|
UISceneNode* sceneNode = UISceneNode::New();
|
|
SceneManager::instance()->add( sceneNode );
|
|
UI::UIThemeManager* themeManager = sceneNode->getUIThemeManager();
|
|
themeManager->setDefaultFont( font );
|
|
|
|
// Create nested tables.
|
|
UIHTMLTable* rootTable = UIHTMLTable::New();
|
|
rootTable->setLayoutSizePolicy( SizePolicy::MatchParent, SizePolicy::WrapContent );
|
|
rootTable->setParent( sceneNode->getRoot() );
|
|
|
|
UIHTMLTable* currentTable = rootTable;
|
|
for ( int i = 0; i < 10; ++i ) {
|
|
UIHTMLTableRow* row = UIHTMLTableRow::New();
|
|
row->setParent( currentTable );
|
|
UIHTMLTableCell* cell = UIHTMLTableCell::New( "td" );
|
|
cell->setParent( row );
|
|
|
|
UIHTMLTable* childTable = UIHTMLTable::New();
|
|
childTable->setLayoutSizePolicy( SizePolicy::MatchParent, SizePolicy::WrapContent );
|
|
childTable->setParent( cell );
|
|
currentTable = childTable;
|
|
}
|
|
|
|
UIHTMLTableRow* row = UIHTMLTableRow::New();
|
|
row->setParent( currentTable );
|
|
UIHTMLTableCell* cell = UIHTMLTableCell::New( "td" );
|
|
cell->setParent( row );
|
|
UITextSpan* span = UITextSpan::New();
|
|
span->setParent( cell );
|
|
span->setText( "Deeply nested text" );
|
|
|
|
Clock clock;
|
|
sceneNode->updateDirtyLayouts();
|
|
|
|
Log::info( "Time for nested layout (10 levels): %.2f ms",
|
|
clock.getElapsedTime().asMilliseconds() );
|
|
|
|
Engine::destroySingleton();
|
|
}
|
|
|
|
UTEST( UIHTMLTable, specifiedWidth ) {
|
|
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" ).get();
|
|
font->loadFromFile( "../assets/fonts/NotoSans-Regular.ttf" );
|
|
ASSERT_TRUE( font != nullptr && font->loaded() );
|
|
FontFamily::loadFromRegular( font );
|
|
|
|
UISceneNode* sceneNode = UISceneNode::New();
|
|
SceneManager::instance()->add( sceneNode );
|
|
UI::UIThemeManager* themeManager = sceneNode->getUIThemeManager();
|
|
themeManager->setDefaultFont( font );
|
|
|
|
sceneNode->loadLayoutFromString(
|
|
R"(<table style="width: 500px; height: wrap-content;">
|
|
<tr>
|
|
<td id="c1">C1</td>
|
|
<td id="c2" style="width: 200px;">C2</td>
|
|
</tr>
|
|
</table>)" );
|
|
|
|
sceneNode->updateDirtyLayouts();
|
|
|
|
auto c1 = sceneNode->getRoot()->find( "c1" );
|
|
auto c2 = sceneNode->getRoot()->find( "c2" );
|
|
|
|
ASSERT_TRUE( c1 != nullptr );
|
|
ASSERT_TRUE( c2 != nullptr );
|
|
|
|
// Cell 2 should be at least 200px.
|
|
EXPECT_GE( c2->getPixelsSize().getWidth(), 200.f );
|
|
// Total width should be 500px (minus padding if any, but default is 0).
|
|
EXPECT_NEAR( c1->getPixelsSize().getWidth() + c2->getPixelsSize().getWidth(), 500.f, 1.f );
|
|
|
|
Engine::destroySingleton();
|
|
}
|
|
|
|
UTEST( UIHTMLTable, nestedSpecifiedWidth ) {
|
|
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" ).get();
|
|
font->loadFromFile( "../assets/fonts/NotoSans-Regular.ttf" );
|
|
ASSERT_TRUE( font != nullptr && font->loaded() );
|
|
FontFamily::loadFromRegular( font );
|
|
|
|
UISceneNode* sceneNode = UISceneNode::New();
|
|
SceneManager::instance()->add( sceneNode );
|
|
UI::UIThemeManager* themeManager = sceneNode->getUIThemeManager();
|
|
themeManager->setDefaultFont( font );
|
|
|
|
sceneNode->loadLayoutFromString(
|
|
R"(<table style="width: 500px; height: wrap-content;">
|
|
<tr>
|
|
<td id="c1"><img style="width: 50px; height: 10px;" /></td>
|
|
<td id="c2">Flexible text content that should take the rest of the space</td>
|
|
</tr>
|
|
</table>)" );
|
|
|
|
sceneNode->updateDirtyLayouts();
|
|
|
|
auto c1 = sceneNode->getRoot()->find( "c1" );
|
|
auto c2 = sceneNode->getRoot()->find( "c2" );
|
|
|
|
ASSERT_TRUE( c1 != nullptr );
|
|
ASSERT_TRUE( c2 != nullptr );
|
|
|
|
// Cell 1 should be exactly 50px because it's rigid (only contains fixed-width image)
|
|
// and Cell 2 is flexible.
|
|
EXPECT_NEAR( c1->getPixelsSize().getWidth(), 50.f, 1.f );
|
|
EXPECT_NEAR( c2->getPixelsSize().getWidth(), 450.f, 1.f );
|
|
|
|
Engine::destroySingleton();
|
|
}
|
|
|
|
UTEST( UIHTMLInput, sizeAttribute ) {
|
|
init_ui_test();
|
|
auto* sceneNode = SceneManager::instance()->getUISceneNode();
|
|
sceneNode->combineStyleSheet( "* { background-color: #fedcba; border-width: 2px; }" );
|
|
sceneNode->loadLayoutFromString( R"html(
|
|
<vbox layout_width="wrap_content" layout_height="wrap_content">
|
|
<input id="i1" size="10" />
|
|
<input id="i2" size="20" />
|
|
<input id="i3" />
|
|
<input id="i_pwd" type="password" />
|
|
<input id="i_mode_pwd" input-mode="password" />
|
|
<input id="i_chk" type="checkbox" />
|
|
<input id="i_chk_checked" type="checkbox" checked="checked" value="enabled" />
|
|
<input id="i_radio_checked" type="radio" checked="checked" />
|
|
</vbox>
|
|
)html" );
|
|
|
|
auto c1 = sceneNode->getRoot()->find( "i1" )->asType<UIHTMLInput>();
|
|
auto c2 = sceneNode->getRoot()->find( "i2" )->asType<UIHTMLInput>();
|
|
auto c3 = sceneNode->getRoot()->find( "i3" )->asType<UIHTMLInput>();
|
|
auto cp = sceneNode->getRoot()->find( "i_pwd" )->asType<UIHTMLInput>();
|
|
auto cm = sceneNode->getRoot()->find( "i_mode_pwd" )->asType<UIHTMLInput>();
|
|
auto cc = sceneNode->getRoot()->find( "i_chk" )->asType<UIHTMLInput>();
|
|
auto ccc = sceneNode->getRoot()->find( "i_chk_checked" )->asType<UIHTMLInput>();
|
|
auto crc = sceneNode->getRoot()->find( "i_radio_checked" )->asType<UIHTMLInput>();
|
|
|
|
ASSERT_TRUE( c1 != nullptr );
|
|
ASSERT_TRUE( c2 != nullptr );
|
|
ASSERT_TRUE( c3 != nullptr );
|
|
ASSERT_TRUE( cp != nullptr );
|
|
ASSERT_TRUE( cm != nullptr );
|
|
ASSERT_TRUE( cc != nullptr );
|
|
ASSERT_TRUE( ccc != nullptr );
|
|
ASSERT_TRUE( crc != nullptr );
|
|
|
|
auto i1 = c1->getChildWidget()->asType<UIHTMLTextInput>();
|
|
auto i2 = c2->getChildWidget()->asType<UIHTMLTextInput>();
|
|
auto i3 = c3->getChildWidget()->asType<UIHTMLTextInput>();
|
|
|
|
ASSERT_TRUE( i1 != nullptr );
|
|
ASSERT_TRUE( i2 != nullptr );
|
|
ASSERT_TRUE( i3 != nullptr );
|
|
|
|
EXPECT_EQ( i1->getHtmlSize(), 10u );
|
|
EXPECT_EQ( i2->getHtmlSize(), 20u );
|
|
EXPECT_EQ( i3->getHtmlSize(), 20u );
|
|
|
|
EXPECT_GT( i2->getPixelsSize().getWidth(), i1->getPixelsSize().getWidth() );
|
|
EXPECT_NEAR( i2->getPixelsSize().getWidth(), i3->getPixelsSize().getWidth(), 1.f );
|
|
|
|
EXPECT_TRUE( cp->getChildWidget()->isType( UI_TYPE_TEXTINPUT ) );
|
|
EXPECT_TRUE( cp->getChildWidget()->asType<UITextInput>()->getMode() ==
|
|
UITextInput::TextInputMode::Password );
|
|
EXPECT_TRUE( cp->getChildWidget()->asType<UITextInput>()->getMode() ==
|
|
UITextInput::TextInputMode::Password );
|
|
EXPECT_TRUE( cm->getChildWidget()->asType<UITextInput>()->getMode() ==
|
|
UITextInput::TextInputMode::Password );
|
|
EXPECT_TRUE( cc->getChildWidget()->isType( UI_TYPE_CHECKBOX ) );
|
|
EXPECT_TRUE( ccc->getChildWidget()->isType( UI_TYPE_CHECKBOX ) );
|
|
EXPECT_TRUE( ccc->getChildWidget()->asType<UICheckBox>()->isChecked() );
|
|
EXPECT_TRUE( ccc->getFormValue() == "enabled" );
|
|
EXPECT_TRUE( crc->getChildWidget()->isType( UI_TYPE_RADIOBUTTON ) );
|
|
EXPECT_TRUE( crc->getChildWidget()->asType<UIRadioButton>()->isActive() );
|
|
EXPECT_TRUE( crc->getFormValue() == "on" );
|
|
|
|
Engine::destroySingleton();
|
|
}
|
|
|
|
UTEST( UIHTML, adjacentBlockMarginsCollapse ) {
|
|
init_ui_test();
|
|
auto* sceneNode = SceneManager::instance()->getUISceneNode();
|
|
sceneNode->loadLayoutFromString( R"html(
|
|
<html><body>
|
|
<div id="container" style="width: 300px; height: wrap-content; padding: 0;">
|
|
<p id="p1" style="font-size: 10px; line-height: 20px; margin: 6px 0;">one</p>
|
|
<p id="p2" style="font-size: 10px; line-height: 20px; margin: 6px 0;">two</p>
|
|
<p id="p3" style="font-size: 10px; line-height: 20px; margin: 6px 0;">three</p>
|
|
</div>
|
|
</body></html>
|
|
)html" );
|
|
sceneNode->updateDirtyLayouts();
|
|
|
|
auto* container = sceneNode->getRoot()->find( "container" )->asType<UIWidget>();
|
|
auto* p1 = sceneNode->getRoot()->find( "p1" )->asType<UIWidget>();
|
|
auto* p2 = sceneNode->getRoot()->find( "p2" )->asType<UIWidget>();
|
|
auto* p3 = sceneNode->getRoot()->find( "p3" )->asType<UIWidget>();
|
|
ASSERT_TRUE( container != nullptr );
|
|
ASSERT_TRUE( p1 != nullptr );
|
|
ASSERT_TRUE( p2 != nullptr );
|
|
ASSERT_TRUE( p3 != nullptr );
|
|
|
|
EXPECT_NEAR( p1->getPixelsPosition().y, 6.f, 0.5f );
|
|
EXPECT_NEAR( p2->getPixelsPosition().y -
|
|
( p1->getPixelsPosition().y + p1->getPixelsSize().getHeight() ),
|
|
6.f, 0.5f );
|
|
EXPECT_NEAR( p3->getPixelsPosition().y -
|
|
( p2->getPixelsPosition().y + p2->getPixelsSize().getHeight() ),
|
|
6.f, 0.5f );
|
|
EXPECT_NEAR( p3->getPixelsPosition().y + p3->getPixelsSize().getHeight(), 78.f, 0.5f );
|
|
|
|
Engine::destroySingleton();
|
|
}
|
|
|
|
UTEST( UIHTML, DataProperties ) {
|
|
init_ui_test();
|
|
auto* sceneNode = SceneManager::instance()->getUISceneNode();
|
|
sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( R"html(
|
|
<html>
|
|
<body>
|
|
<div id="target"
|
|
data-role="hero"
|
|
data-tags="featured primary"
|
|
data-lang="en-US"
|
|
data-language="cpp"
|
|
data-id="user-42"
|
|
data-empty="">
|
|
</div>
|
|
<div id="missing-control"></div>
|
|
</body>
|
|
</html>
|
|
)html" ) );
|
|
|
|
auto* target = sceneNode->getRoot()->find( "target" )->asType<UIHTMLWidget>();
|
|
ASSERT_TRUE( target != nullptr );
|
|
|
|
EXPECT_TRUE( target->hasDataProperty( "data-role" ) );
|
|
EXPECT_TRUE( target->hasDataProperty( "DATA-ROLE" ) );
|
|
EXPECT_TRUE( target->hasDataProperty( "data-empty" ) );
|
|
EXPECT_FALSE( target->hasDataProperty( "data-missing" ) );
|
|
EXPECT_TRUE( target->getDataPropertyString( "data-role" ) == "hero" );
|
|
EXPECT_TRUE( target->getDataPropertyString( "data-empty", "fallback" ) == "" );
|
|
EXPECT_TRUE( target->getDataPropertyString( "data-missing", "fallback" ) == "fallback" );
|
|
EXPECT_TRUE( target->getPropertyString( "data-role" ) == "hero" );
|
|
|
|
EXPECT_EQ( sceneNode->getRoot()->querySelectorAll( "[data-role]" ).size(), (size_t)1 );
|
|
EXPECT_EQ( sceneNode->getRoot()->querySelectorAll( "[data-role=\"hero\"]" ).size(), (size_t)1 );
|
|
EXPECT_EQ( sceneNode->getRoot()->querySelectorAll( "[data-tags~=\"featured\"]" ).size(),
|
|
(size_t)1 );
|
|
EXPECT_EQ( sceneNode->getRoot()->querySelectorAll( "[data-lang|=\"en\"]" ).size(), (size_t)1 );
|
|
EXPECT_EQ( sceneNode->getRoot()->querySelectorAll( "[data-id^=\"user-\"]" ).size(), (size_t)1 );
|
|
EXPECT_EQ( sceneNode->getRoot()->querySelectorAll( "[data-id$=\"-42\"]" ).size(), (size_t)1 );
|
|
EXPECT_EQ( sceneNode->getRoot()->querySelectorAll( "[data-id*=\"ser\"]" ).size(), (size_t)1 );
|
|
EXPECT_EQ( sceneNode->getRoot()->querySelectorAll( "[data-empty]" ).size(), (size_t)1 );
|
|
EXPECT_EQ( sceneNode->getRoot()->querySelectorAll( "[data-missing]" ).size(), (size_t)0 );
|
|
EXPECT_TRUE( StyleSheetSelector( "[width]" ).select( target, false ) );
|
|
EXPECT_TRUE( StyleSheetSelector( "[width=\"" + target->getPropertyString( "width" ) + "\"]" )
|
|
.select( target, false ) );
|
|
EXPECT_FALSE( StyleSheetSelector( "[unknown-property]" ).select( target, false ) );
|
|
|
|
EXPECT_TRUE( target->getDataPropertyString( "data-language" ) == "cpp" );
|
|
|
|
Engine::destroySingleton();
|
|
}
|
|
|
|
UTEST( UIHTMLDetails, closedByDefault ) {
|
|
init_ui_test();
|
|
auto* sceneNode = SceneManager::instance()->getUISceneNode();
|
|
sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( R"html(
|
|
<details id="d"><summary id="s">Label</summary><p id="p">Content</p></details>
|
|
)html" ) );
|
|
sceneNode->updateDirtyLayouts();
|
|
|
|
auto* details = sceneNode->getRoot()->find( "d" )->asType<UIHTMLDetails>();
|
|
auto* summary = sceneNode->getRoot()->find( "s" )->asType<UIHTMLSummary>();
|
|
auto* content = sceneNode->getRoot()->find( "p" )->asType<UIWidget>();
|
|
ASSERT_TRUE( details != nullptr );
|
|
ASSERT_TRUE( summary != nullptr );
|
|
ASSERT_TRUE( content != nullptr );
|
|
EXPECT_FALSE( details->isOpen() );
|
|
EXPECT_TRUE( summary->isVisible() );
|
|
EXPECT_FALSE( content->isVisible() );
|
|
|
|
Engine::destroySingleton();
|
|
}
|
|
|
|
UTEST( UIHTMLDetails, summaryListStyleType ) {
|
|
init_ui_test();
|
|
auto* sceneNode = SceneManager::instance()->getUISceneNode();
|
|
sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( R"html(
|
|
<details id="d">
|
|
<summary id="s1">Default</summary>
|
|
<summary id="s2" style="list-style-type: disclosure-open;">Open marker</summary>
|
|
<summary id="s3" style="list-style-type: decimal;">Decimal marker</summary>
|
|
</details>
|
|
)html" ) );
|
|
sceneNode->updateDirtyLayouts();
|
|
|
|
const auto* propDef = StyleSheetSpecification::instance()->getProperty( "list-style-type" );
|
|
ASSERT_TRUE( propDef != nullptr );
|
|
|
|
auto* s1 = sceneNode->getRoot()->find( "s1" )->asType<UIHTMLSummary>();
|
|
auto* s2 = sceneNode->getRoot()->find( "s2" )->asType<UIHTMLSummary>();
|
|
auto* s3 = sceneNode->getRoot()->find( "s3" )->asType<UIHTMLSummary>();
|
|
ASSERT_TRUE( s1 != nullptr );
|
|
ASSERT_TRUE( s2 != nullptr );
|
|
ASSERT_TRUE( s3 != nullptr );
|
|
|
|
EXPECT_TRUE( s1->getPropertyString( propDef ) == "disclosure-closed" );
|
|
EXPECT_TRUE( s2->getPropertyString( propDef ) == "disclosure-open" );
|
|
EXPECT_TRUE( s3->getPropertyString( propDef ) == "decimal" );
|
|
|
|
Engine::destroySingleton();
|
|
}
|
|
|
|
UTEST( UIHTMLDetails, summaryListStyleNoneClearsDefaultPadding ) {
|
|
init_ui_test();
|
|
auto* sceneNode = SceneManager::instance()->getUISceneNode();
|
|
sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( R"html(
|
|
<html>
|
|
<body>
|
|
<details>
|
|
<summary id="default">Default marker</summary>
|
|
<summary id="none" style="list-style-type: none;">No marker</summary>
|
|
<summary id="explicit" style="list-style-type: none; padding-left: 7px;">
|
|
Explicit padding
|
|
</summary>
|
|
</details>
|
|
</body>
|
|
</html>
|
|
)html" ) );
|
|
sceneNode->updateDirtyLayouts();
|
|
|
|
auto* defaultSummary = sceneNode->getRoot()->find( "default" )->asType<UIHTMLSummary>();
|
|
auto* noneSummary = sceneNode->getRoot()->find( "none" )->asType<UIHTMLSummary>();
|
|
auto* explicitSummary = sceneNode->getRoot()->find( "explicit" )->asType<UIHTMLSummary>();
|
|
ASSERT_TRUE( defaultSummary != nullptr );
|
|
ASSERT_TRUE( noneSummary != nullptr );
|
|
ASSERT_TRUE( explicitSummary != nullptr );
|
|
|
|
EXPECT_GT( defaultSummary->getPixelsPadding().Left, 0.f );
|
|
EXPECT_NEAR( noneSummary->getPixelsPadding().Left, 0.f, 0.5f );
|
|
EXPECT_NEAR( explicitSummary->getPixelsPadding().Left, 7.f, 0.5f );
|
|
|
|
Engine::destroySingleton();
|
|
}
|
|
|
|
UTEST( UIHTMLDetails, inlineBlockSummaryListStyleNoneSize ) {
|
|
init_ui_test();
|
|
auto* sceneNode = SceneManager::instance()->getUISceneNode();
|
|
std::string html;
|
|
FileSystem::fileGet( "assets/html/lobsters_item.html", html );
|
|
sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( html ) );
|
|
sceneNode->updateDirtyLayouts();
|
|
|
|
auto* details = sceneNode->getRoot()->querySelector( ".caches" )->asType<UIHTMLDetails>();
|
|
auto* summary =
|
|
sceneNode->getRoot()->querySelector( ".caches summary" )->asType<UIHTMLSummary>();
|
|
auto* author = sceneNode->getRoot()->querySelector( ".u-author" )->asType<UIWidget>();
|
|
auto* time = sceneNode->getRoot()->querySelector( "time" )->asType<UIWidget>();
|
|
ASSERT_TRUE( details != nullptr );
|
|
ASSERT_TRUE( summary != nullptr );
|
|
ASSERT_TRUE( author != nullptr );
|
|
ASSERT_TRUE( time != nullptr );
|
|
|
|
EXPECT_EQ( details->getDisplay(), CSSDisplay::InlineBlock );
|
|
EXPECT_EQ( summary->getListStyleType(), CSSListStyleType::None );
|
|
EXPECT_NEAR( summary->getPixelsPadding().Left, 0.f, 0.5f );
|
|
EXPECT_LE( details->getPixelsSize().getHeight(),
|
|
eemax( author->getPixelsSize().getHeight(), time->getPixelsSize().getHeight() ) +
|
|
1.f );
|
|
EXPECT_NEAR( details->getPixelsPosition().y, author->getPixelsPosition().y, 2.f );
|
|
EXPECT_NEAR( details->getPixelsPosition().y, time->getPixelsPosition().y, 2.f );
|
|
|
|
Engine::destroySingleton();
|
|
}
|
|
|
|
UTEST( UIHTMLDetails, openAttribute ) {
|
|
init_ui_test();
|
|
auto* sceneNode = SceneManager::instance()->getUISceneNode();
|
|
sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( R"html(
|
|
<details id="d" open><summary>Label</summary><p id="p">Content</p></details>
|
|
)html" ) );
|
|
sceneNode->updateDirtyLayouts();
|
|
|
|
auto* details = sceneNode->getRoot()->find( "d" )->asType<UIHTMLDetails>();
|
|
auto* content = sceneNode->getRoot()->find( "p" )->asType<UIWidget>();
|
|
ASSERT_TRUE( details != nullptr );
|
|
ASSERT_TRUE( content != nullptr );
|
|
EXPECT_TRUE( details->isOpen() );
|
|
EXPECT_TRUE( content->isVisible() );
|
|
|
|
Engine::destroySingleton();
|
|
}
|
|
|
|
UTEST( UIHTMLDetails, openAttributeExplicitFalse ) {
|
|
init_ui_test();
|
|
auto* sceneNode = SceneManager::instance()->getUISceneNode();
|
|
sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( R"html(
|
|
<details id="d" open="false"><summary>Label</summary><p id="p">Content</p></details>
|
|
)html" ) );
|
|
sceneNode->updateDirtyLayouts();
|
|
|
|
auto* details = sceneNode->getRoot()->find( "d" )->asType<UIHTMLDetails>();
|
|
auto* content = sceneNode->getRoot()->find( "p" )->asType<UIWidget>();
|
|
ASSERT_TRUE( details != nullptr );
|
|
ASSERT_TRUE( content != nullptr );
|
|
EXPECT_FALSE( details->isOpen() );
|
|
EXPECT_FALSE( content->isVisible() );
|
|
|
|
Engine::destroySingleton();
|
|
}
|
|
|
|
UTEST( UIHTMLDetails, toggleViaMouse ) {
|
|
init_ui_test();
|
|
auto* sceneNode = SceneManager::instance()->getUISceneNode();
|
|
sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( R"html(
|
|
<details id="d"><summary id="s">Label</summary><p id="p">Content</p></details>
|
|
)html" ) );
|
|
sceneNode->updateDirtyLayouts();
|
|
|
|
auto* details = sceneNode->getRoot()->find( "d" )->asType<UIHTMLDetails>();
|
|
auto* summary = sceneNode->getRoot()->find( "s" )->asType<UIHTMLSummary>();
|
|
auto* content = sceneNode->getRoot()->find( "p" )->asType<UIWidget>();
|
|
ASSERT_TRUE( details != nullptr );
|
|
ASSERT_TRUE( summary != nullptr );
|
|
ASSERT_TRUE( content != nullptr );
|
|
|
|
summary->onMouseClick( summary->getPixelsPosition().asInt(), EE_BUTTON_LMASK );
|
|
EXPECT_TRUE( details->isOpen() );
|
|
EXPECT_TRUE( content->isVisible() );
|
|
summary->onMouseClick( summary->getPixelsPosition().asInt(), EE_BUTTON_LMASK );
|
|
EXPECT_FALSE( details->isOpen() );
|
|
EXPECT_FALSE( content->isVisible() );
|
|
|
|
Engine::destroySingleton();
|
|
}
|
|
|
|
UTEST( UIHTMLDetails, toggleViaKeyboard ) {
|
|
init_ui_test();
|
|
auto* sceneNode = SceneManager::instance()->getUISceneNode();
|
|
sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( R"html(
|
|
<details id="d"><summary id="s">Label</summary><p id="p">Content</p></details>
|
|
)html" ) );
|
|
sceneNode->updateDirtyLayouts();
|
|
|
|
auto* details = sceneNode->getRoot()->find( "d" )->asType<UIHTMLDetails>();
|
|
auto* summary = sceneNode->getRoot()->find( "s" )->asType<UIHTMLSummary>();
|
|
ASSERT_TRUE( details != nullptr );
|
|
ASSERT_TRUE( summary != nullptr );
|
|
|
|
KeyEvent enter( summary, Event::KeyDown, KEY_RETURN, SCANCODE_RETURN, 0, 0 );
|
|
KeyEvent space( summary, Event::KeyDown, KEY_SPACE, SCANCODE_SPACE, 0, 0 );
|
|
EXPECT_EQ( summary->onKeyDown( enter ), 1u );
|
|
EXPECT_TRUE( details->isOpen() );
|
|
EXPECT_EQ( summary->onKeyDown( space ), 1u );
|
|
EXPECT_FALSE( details->isOpen() );
|
|
|
|
Engine::destroySingleton();
|
|
}
|
|
|
|
UTEST( UIHTMLDetails, toggleEvent ) {
|
|
init_ui_test();
|
|
auto* details = UIHTMLDetails::New();
|
|
details->setParent( SceneManager::instance()->getUISceneNode()->getRoot() );
|
|
int toggleCount = 0;
|
|
details->on( Event::OnToggle, [&toggleCount]( const Event* ) { toggleCount++; } );
|
|
|
|
details->setOpen( true );
|
|
details->setOpen( true );
|
|
details->setOpen( false );
|
|
EXPECT_EQ( toggleCount, 2 );
|
|
|
|
Engine::destroySingleton();
|
|
}
|
|
|
|
UTEST( UIHTMLDetails, autoSummary ) {
|
|
init_ui_test();
|
|
auto* sceneNode = SceneManager::instance()->getUISceneNode();
|
|
sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( R"html(
|
|
<details id="d"><p id="p">Content</p></details>
|
|
)html" ) );
|
|
sceneNode->updateDirtyLayouts();
|
|
|
|
auto* details = sceneNode->getRoot()->find( "d" )->asType<UIHTMLDetails>();
|
|
auto* content = sceneNode->getRoot()->find( "p" )->asType<UIWidget>();
|
|
ASSERT_TRUE( details != nullptr );
|
|
ASSERT_TRUE( content != nullptr );
|
|
auto* summary = details->findSummaryChild();
|
|
ASSERT_TRUE( summary != nullptr );
|
|
EXPECT_TRUE( summary->isVisible() );
|
|
EXPECT_FALSE( content->isVisible() );
|
|
EXPECT_TRUE( summary->toggleParentDetails() );
|
|
EXPECT_TRUE( details->isOpen() );
|
|
EXPECT_TRUE( content->isVisible() );
|
|
|
|
Engine::destroySingleton();
|
|
}
|
|
|
|
UTEST( UIHTMLDetails, multipleSummaries ) {
|
|
init_ui_test();
|
|
auto* sceneNode = SceneManager::instance()->getUISceneNode();
|
|
sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( R"html(
|
|
<details id="d">
|
|
<summary id="s1">One</summary>
|
|
<summary id="s2">Two</summary>
|
|
<p id="p">Content</p>
|
|
</details>
|
|
)html" ) );
|
|
sceneNode->updateDirtyLayouts();
|
|
|
|
auto* details = sceneNode->getRoot()->find( "d" )->asType<UIHTMLDetails>();
|
|
auto* s1 = sceneNode->getRoot()->find( "s1" )->asType<UIHTMLSummary>();
|
|
auto* s2 = sceneNode->getRoot()->find( "s2" )->asType<UIHTMLSummary>();
|
|
ASSERT_TRUE( details != nullptr );
|
|
ASSERT_TRUE( s1 != nullptr );
|
|
ASSERT_TRUE( s2 != nullptr );
|
|
EXPECT_TRUE( s1->isVisible() );
|
|
EXPECT_FALSE( s2->isVisible() );
|
|
EXPECT_TRUE( s1->toggleParentDetails() );
|
|
EXPECT_TRUE( details->isOpen() );
|
|
EXPECT_TRUE( s2->isVisible() );
|
|
EXPECT_FALSE( s2->toggleParentDetails() );
|
|
EXPECT_TRUE( details->isOpen() );
|
|
|
|
Engine::destroySingleton();
|
|
}
|
|
|
|
UTEST( UIHTMLDetails, nested ) {
|
|
init_ui_test();
|
|
auto* sceneNode = SceneManager::instance()->getUISceneNode();
|
|
sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( R"html(
|
|
<details id="outer" open>
|
|
<summary id="outer_s">Outer</summary>
|
|
<details id="inner">
|
|
<summary id="inner_s">Inner</summary>
|
|
<p id="inner_p">Inner content</p>
|
|
</details>
|
|
</details>
|
|
)html" ) );
|
|
sceneNode->updateDirtyLayouts();
|
|
|
|
auto* outer = sceneNode->getRoot()->find( "outer" )->asType<UIHTMLDetails>();
|
|
auto* inner = sceneNode->getRoot()->find( "inner" )->asType<UIHTMLDetails>();
|
|
auto* innerSummary = sceneNode->getRoot()->find( "inner_s" )->asType<UIHTMLSummary>();
|
|
ASSERT_TRUE( outer != nullptr );
|
|
ASSERT_TRUE( inner != nullptr );
|
|
ASSERT_TRUE( innerSummary != nullptr );
|
|
EXPECT_TRUE( outer->isOpen() );
|
|
EXPECT_FALSE( inner->isOpen() );
|
|
EXPECT_TRUE( innerSummary->toggleParentDetails() );
|
|
EXPECT_TRUE( outer->isOpen() );
|
|
EXPECT_TRUE( inner->isOpen() );
|
|
|
|
Engine::destroySingleton();
|
|
}
|
|
|
|
UTEST( UIHTMLDetails, hiddenChildPreserved ) {
|
|
init_ui_test();
|
|
auto* sceneNode = SceneManager::instance()->getUISceneNode();
|
|
sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( R"html(
|
|
<details id="d" open><summary>Label</summary><p id="p" hidden>Content</p></details>
|
|
)html" ) );
|
|
sceneNode->updateDirtyLayouts();
|
|
|
|
auto* details = sceneNode->getRoot()->find( "d" )->asType<UIHTMLDetails>();
|
|
auto* content = sceneNode->getRoot()->find( "p" )->asType<UIWidget>();
|
|
ASSERT_TRUE( details != nullptr );
|
|
ASSERT_TRUE( content != nullptr );
|
|
EXPECT_FALSE( content->isVisible() );
|
|
details->setOpen( false );
|
|
details->setOpen( true );
|
|
EXPECT_FALSE( content->isVisible() );
|
|
|
|
Engine::destroySingleton();
|
|
}
|
|
|
|
UTEST( UIHTMLDetails, dynamicChildAddedWhileClosed ) {
|
|
init_ui_test();
|
|
auto* details = UIHTMLDetails::New();
|
|
details->setParent( SceneManager::instance()->getUISceneNode()->getRoot() );
|
|
auto* summary = UIHTMLSummary::New();
|
|
summary->setParent( details );
|
|
details->setOpen( false );
|
|
auto* content = UIRichText::NewParagraph();
|
|
content->setParent( details );
|
|
details->updateLayout();
|
|
|
|
EXPECT_TRUE( summary->isVisible() );
|
|
EXPECT_FALSE( content->isVisible() );
|
|
|
|
Engine::destroySingleton();
|
|
}
|
|
|
|
UTEST( UIHTMLTextArea, rowsColsAttribute ) {
|
|
init_ui_test();
|
|
auto* scene = SceneManager::instance()->getUISceneNode();
|
|
auto* c1_raw = scene->loadLayoutFromString( R"html(
|
|
<vbox layout_width="wrap_content" layout_height="wrap_content">
|
|
<textarea id="t1" rows="2" cols="20"></textarea>
|
|
<textarea id="t2" rows="4" cols="40"></textarea>
|
|
</vbox>
|
|
)html" );
|
|
ASSERT_TRUE( c1_raw != nullptr );
|
|
auto* t1 = c1_raw->find( "t1" )->asType<UIHTMLTextArea>();
|
|
auto* t2 = c1_raw->find( "t2" )->asType<UIHTMLTextArea>();
|
|
ASSERT_TRUE( t1 != nullptr );
|
|
ASSERT_TRUE( t2 != nullptr );
|
|
EXPECT_EQ( t1->getRows(), 2u );
|
|
EXPECT_EQ( t1->getCols(), 20u );
|
|
EXPECT_EQ( t2->getRows(), 4u );
|
|
EXPECT_EQ( t2->getCols(), 40u );
|
|
EXPECT_GT( t2->getPixelsSize().getWidth(), t1->getPixelsSize().getWidth() );
|
|
EXPECT_GT( t2->getPixelsSize().getHeight(), t1->getPixelsSize().getHeight() );
|
|
|
|
Engine::destroySingleton();
|
|
}
|
|
|
|
UTEST( UIHTML, FormControlsDefaultInlineBlock ) {
|
|
init_ui_test();
|
|
auto* sceneNode = SceneManager::instance()->getUISceneNode();
|
|
sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( R"html(
|
|
<div id="container" style="width: 600px;">
|
|
<input id="i1" size="5">
|
|
<input id="i2" size="5">
|
|
<textarea id="t1" rows="2" cols="8"></textarea>
|
|
<textarea id="t2" rows="2" cols="8"></textarea>
|
|
</div>
|
|
)html" ) );
|
|
sceneNode->updateDirtyLayouts();
|
|
|
|
auto* i1 = sceneNode->getRoot()->find( "i1" )->asType<UIHTMLInput>();
|
|
auto* i2 = sceneNode->getRoot()->find( "i2" )->asType<UIHTMLInput>();
|
|
auto* t1 = sceneNode->getRoot()->find( "t1" )->asType<UIHTMLTextArea>();
|
|
auto* t2 = sceneNode->getRoot()->find( "t2" )->asType<UIHTMLTextArea>();
|
|
|
|
ASSERT_TRUE( i1 != nullptr );
|
|
ASSERT_TRUE( i2 != nullptr );
|
|
ASSERT_TRUE( t1 != nullptr );
|
|
ASSERT_TRUE( t2 != nullptr );
|
|
|
|
EXPECT_EQ( i1->getDisplay(), CSSDisplay::InlineBlock );
|
|
EXPECT_EQ( i2->getDisplay(), CSSDisplay::InlineBlock );
|
|
|
|
EXPECT_EQ( i1->getPixelsPosition().y, i2->getPixelsPosition().y );
|
|
EXPECT_LT( i1->getPixelsPosition().x, i2->getPixelsPosition().x );
|
|
EXPECT_EQ( t1->getPixelsPosition().y, t2->getPixelsPosition().y );
|
|
EXPECT_LT( t1->getPixelsPosition().x, t2->getPixelsPosition().x );
|
|
|
|
Engine::destroySingleton();
|
|
}
|
|
|
|
UTEST( UIHTMLInput, hostOwnsCSSBox ) {
|
|
init_ui_test();
|
|
auto* sceneNode = SceneManager::instance()->getUISceneNode();
|
|
sceneNode->loadLayoutFromString( R"html(
|
|
<vbox layout_width="wrap_content" layout_height="wrap_content">
|
|
<input id="styled_input" type="text"
|
|
style="background-color: #123456; border: 3px solid #abcdef; padding: 5px;" />
|
|
</vbox>
|
|
)html" );
|
|
sceneNode->updateDirtyLayouts();
|
|
|
|
auto* input = sceneNode->getRoot()->find( "styled_input" )->asType<UIHTMLInput>();
|
|
ASSERT_TRUE( input != nullptr );
|
|
auto* implementation = input->getChildWidget();
|
|
ASSERT_TRUE( implementation != nullptr );
|
|
|
|
EXPECT_TRUE( input->hasBackground() );
|
|
EXPECT_TRUE( input->hasBorder() );
|
|
EXPECT_FALSE( implementation->hasBackground() );
|
|
EXPECT_FALSE( implementation->hasBorder() );
|
|
EXPECT_TRUE( 0 == ( implementation->getFlags() & UI_HTML_ELEMENT ) );
|
|
EXPECT_NEAR( input->getPixelsPadding().Left, 5.f, 0.01f );
|
|
EXPECT_NEAR( input->getPixelsPadding().Top, 5.f, 0.01f );
|
|
EXPECT_NEAR( input->getPixelsPadding().Right, 5.f, 0.01f );
|
|
EXPECT_NEAR( input->getPixelsPadding().Bottom, 5.f, 0.01f );
|
|
EXPECT_TRUE( implementation->getPadding() == Rectf() );
|
|
EXPECT_NEAR( implementation->getPixelsPosition().x, 8.f, 0.01f );
|
|
EXPECT_NEAR( implementation->getPixelsPosition().y, 8.f, 0.01f );
|
|
EXPECT_NEAR( implementation->getPixelsSize().getWidth(),
|
|
input->getPixelsSize().getWidth() - 16.f, 0.01f );
|
|
EXPECT_NEAR( implementation->getPixelsSize().getHeight(),
|
|
input->getPixelsSize().getHeight() - 16.f, 0.01f );
|
|
|
|
input->setInputType( "email" );
|
|
EXPECT_STDSTREQ( input->getInputType(), "email" );
|
|
EXPECT_TRUE( input->getChildWidget()->isType( UI_TYPE_TEXTINPUT ) );
|
|
|
|
Engine::destroySingleton();
|
|
}
|
|
|
|
UTEST( UIHTMLInput, inlineTextUsesReplacedControlBaselineAndFontMetrics ) {
|
|
init_ui_test();
|
|
auto* sceneNode = SceneManager::instance()->getUISceneNode();
|
|
sceneNode->loadLayoutFromString( R"html(
|
|
<div id="line" style="font-size: 14px; line-height: 20px;">
|
|
<span id="label">label</span><input id="input" type="text"
|
|
style="font-size: 14px; border: 1px solid gray; padding: 3px 2px;" />
|
|
</div>
|
|
)html" );
|
|
sceneNode->updateDirtyLayouts();
|
|
|
|
auto* line = sceneNode->getRoot()->find( "line" )->asType<UIHTMLWidget>();
|
|
auto* label = sceneNode->getRoot()->find( "label" )->asType<UITextSpan>();
|
|
auto* input = sceneNode->getRoot()->find( "input" )->asType<UIHTMLInput>();
|
|
ASSERT_TRUE( line != nullptr );
|
|
ASSERT_TRUE( label != nullptr );
|
|
ASSERT_TRUE( input != nullptr );
|
|
ASSERT_TRUE( input->getChildWidget()->isType( UI_TYPE_HTML_TEXTINPUT ) );
|
|
auto* textInput = input->getChildWidget()->asType<UIHTMLTextInput>();
|
|
ASSERT_TRUE( textInput->getFont() != nullptr );
|
|
|
|
const Float expectedContentHeight =
|
|
std::ceil( textInput->getFont()->getAscent( textInput->getFontSize() ) +
|
|
textInput->getFont()->getDescent( textInput->getFontSize() ) );
|
|
EXPECT_NEAR( textInput->getPixelsSize().getHeight(), expectedContentHeight, 0.01f );
|
|
EXPECT_NEAR( input->getPixelsSize().getHeight(),
|
|
expectedContentHeight + input->getPixelsContentOffset().Top +
|
|
input->getPixelsContentOffset().Bottom,
|
|
0.01f );
|
|
|
|
const auto& lines = line->getRichTextPtr()->getLines();
|
|
ASSERT_EQ( lines.size(), (size_t)1 );
|
|
const Float lineBaseline = line->convertToWorldSpace( Vector2f::Zero ).y +
|
|
line->getPixelsContentOffset().Top + lines.front().y +
|
|
lines.front().maxAscent;
|
|
const Float inputBaseline =
|
|
input->convertToWorldSpace( Vector2f::Zero ).y + input->getReplacedElementBaseline();
|
|
EXPECT_NEAR( inputBaseline, lineBaseline, 0.5f );
|
|
|
|
Engine::destroySingleton();
|
|
}
|
|
|
|
UTEST( UIHTMLInput, dynamicTypeKeepsHostAndState ) {
|
|
init_ui_test();
|
|
auto* input = UIHTMLInput::New();
|
|
input->setParent( SceneManager::instance()->getUISceneNode()->getRoot() );
|
|
static_cast<UITextInput*>( input->getChildWidget() )->setText( "edited value" );
|
|
|
|
UIHTMLInput* originalHost = input;
|
|
input->setInputType( "checkbox" );
|
|
EXPECT_EQ( input, originalHost );
|
|
EXPECT_TRUE( input->getChildWidget()->isType( UI_TYPE_CHECKBOX ) );
|
|
|
|
input->setInputType( "hidden" );
|
|
EXPECT_FALSE( input->isVisible() );
|
|
EXPECT_FALSE( input->isEnabled() );
|
|
EXPECT_EQ( input->getDisplay(), CSSDisplay::None );
|
|
|
|
input->setInputType( "text" );
|
|
EXPECT_EQ( input, originalHost );
|
|
EXPECT_TRUE( input->isVisible() );
|
|
EXPECT_TRUE( input->isEnabled() );
|
|
EXPECT_EQ( input->getDisplay(), CSSDisplay::InlineBlock );
|
|
ASSERT_TRUE( input->getChildWidget()->isType( UI_TYPE_TEXTINPUT ) );
|
|
EXPECT_TRUE( static_cast<UITextInput*>( input->getChildWidget() )->getText() ==
|
|
"edited value" );
|
|
|
|
input->setInputType( "unsupported-type" );
|
|
EXPECT_STDSTREQ( input->getInputType(), "text" );
|
|
|
|
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" ).get();
|
|
font->loadFromFile( "../assets/fonts/NotoSans-Regular.ttf" );
|
|
ASSERT_TRUE( font != nullptr && font->loaded() );
|
|
FontFamily::loadFromRegular( font );
|
|
|
|
UISceneNode* sceneNode = 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();
|
|
}
|
|
|
|
UTEST( UIHTMLBody, backgroundColorPropagation ) {
|
|
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" ).get();
|
|
font->loadFromFile( "../assets/fonts/NotoSans-Regular.ttf" );
|
|
ASSERT_TRUE( font != nullptr && font->loaded() );
|
|
FontFamily::loadFromRegular( font );
|
|
|
|
UISceneNode* sceneNode = UISceneNode::New();
|
|
SceneManager::instance()->add( sceneNode );
|
|
UI::UIThemeManager* themeManager = sceneNode->getUIThemeManager();
|
|
themeManager->setDefaultFont( font );
|
|
|
|
sceneNode->loadLayoutFromString(
|
|
R"(<html id="html_el">
|
|
<body id="body_el" style="background-color: red; max-width: 960px;">
|
|
</body>
|
|
</html>)" );
|
|
|
|
sceneNode->updateDirtyLayouts();
|
|
|
|
auto html_el = sceneNode->getRoot()->find( "html_el" );
|
|
auto body_el = sceneNode->getRoot()->find( "body_el" );
|
|
|
|
ASSERT_TRUE( html_el != nullptr );
|
|
ASSERT_TRUE( body_el != nullptr );
|
|
|
|
// HTML element should have inherited the red background color, and body should be transparent
|
|
EXPECT_TRUE( html_el->asType<UIWidget>()->getBackgroundColor() == Color::Red );
|
|
EXPECT_TRUE( body_el->asType<UIWidget>()->getBackgroundColor() == Color::Transparent );
|
|
|
|
Engine::destroySingleton();
|
|
}
|
|
|
|
UTEST( UIHTMLBody, maxWidthResizingBug ) {
|
|
Engine::instance()->createWindow( WindowSettings( 1024, 768, "HTML Resize Bug",
|
|
WindowStyle::Default, WindowBackend::Default,
|
|
32, {}, 1, false, true ) );
|
|
FileSystem::changeWorkingDirectory( Sys::getProcessPath() );
|
|
|
|
UISceneNode* sceneNode = UISceneNode::New();
|
|
SceneManager::instance()->add( sceneNode );
|
|
|
|
UI::CSS::StyleSheetParser parser;
|
|
parser.loadFromFile( "assets/html/dwarmstrong/style.css" );
|
|
sceneNode->setStyleSheet( parser.getStyleSheet() );
|
|
|
|
std::string htmlContent;
|
|
FileSystem::fileGet( "assets/html/dwarmstrong/dwarmstrong.html", htmlContent );
|
|
sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( htmlContent ) );
|
|
|
|
sceneNode->getRoot()->setSize( 1024, 768 );
|
|
sceneNode->updateDirtyLayouts();
|
|
|
|
auto body_el = sceneNode->getRoot()->findByType( UI_TYPE_HTML_BODY )->asType<UIWidget>();
|
|
ASSERT_TRUE( body_el != nullptr );
|
|
Float widthAt1024 = body_el->getPixelsSize().getWidth();
|
|
EXPECT_NEAR( widthAt1024, 960.f,
|
|
10.f ); // It should be around 960px (minus some margins if any)
|
|
|
|
sceneNode->getRoot()->setSize( 2048, 768 );
|
|
sceneNode->updateDirtyLayouts();
|
|
Float widthAt2048 = body_el->getPixelsSize().getWidth();
|
|
EXPECT_NEAR( widthAt2048, 960.f, 10.f ); // Body should stay 960px even when parent is huge
|
|
|
|
sceneNode->getRoot()->setSize( 1024, 768 );
|
|
sceneNode->updateDirtyLayouts();
|
|
|
|
Float widthAfterResize = body_el->getPixelsSize().getWidth();
|
|
EXPECT_NEAR( widthAt1024, widthAfterResize, 1.f );
|
|
|
|
Engine::destroySingleton();
|
|
}
|
|
|
|
UTEST( UILayout, marginAuto ) {
|
|
Engine::instance()->createWindow( WindowSettings( 1024, 650, "Margin Auto Test",
|
|
WindowStyle::Default, WindowBackend::Default,
|
|
32, {}, 1, false, true ) );
|
|
FileSystem::changeWorkingDirectory( Sys::getProcessPath() );
|
|
|
|
FontTrueType* font = FontTrueType::New( "NotoSans-Regular" ).get();
|
|
font->loadFromFile( "../assets/fonts/NotoSans-Regular.ttf" );
|
|
ASSERT_TRUE( font != nullptr && font->loaded() );
|
|
FontFamily::loadFromRegular( font );
|
|
|
|
UISceneNode* sceneNode = UISceneNode::New();
|
|
SceneManager::instance()->add( sceneNode );
|
|
UI::UIThemeManager* themeManager = sceneNode->getUIThemeManager();
|
|
themeManager->setDefaultFont( font );
|
|
|
|
auto* container = sceneNode->loadLayoutFromString(
|
|
R"(<vbox id="container">
|
|
<widget id="child" style="margin: 0 auto;" />
|
|
</vbox>)" );
|
|
|
|
auto child = sceneNode->getRoot()->find( "child" );
|
|
ASSERT_TRUE( child != nullptr );
|
|
|
|
UIWidget* childWidget = child->asType<UIWidget>();
|
|
UIWidget* contWidget = container->asType<UIWidget>();
|
|
|
|
contWidget->setSize( 500, 500 );
|
|
childWidget->setSize( 100, 100 );
|
|
sceneNode->updateDirtyLayouts();
|
|
|
|
Float expectedMarginX =
|
|
( contWidget->getPixelsSize().getWidth() - childWidget->getPixelsSize().getWidth() ) / 2.f;
|
|
|
|
// Margin left/right should be auto computed to expectedMarginX
|
|
EXPECT_NEAR( childWidget->getLayoutPixelsMargin().Left, expectedMarginX, 1.f );
|
|
EXPECT_NEAR( childWidget->getLayoutPixelsMargin().Right, expectedMarginX, 1.f );
|
|
EXPECT_NEAR( childWidget->getLayoutPixelsMargin().Top, 0.f, 1.f );
|
|
EXPECT_NEAR( childWidget->getLayoutPixelsMargin().Bottom, 0.f, 1.f );
|
|
|
|
// Resize parent and see if margins re-evaluate automatically
|
|
contWidget->setSize( 800, 800 );
|
|
sceneNode->updateDirtyLayouts();
|
|
|
|
expectedMarginX =
|
|
( contWidget->getPixelsSize().getWidth() - childWidget->getPixelsSize().getWidth() ) / 2.f;
|
|
|
|
EXPECT_NEAR( childWidget->getLayoutPixelsMargin().Left, expectedMarginX, 1.f );
|
|
EXPECT_NEAR( childWidget->getLayoutPixelsMargin().Right, expectedMarginX, 1.f );
|
|
|
|
// Now test resize of child
|
|
childWidget->setSize( 200, 100 );
|
|
sceneNode->updateDirtyLayouts();
|
|
|
|
expectedMarginX =
|
|
( contWidget->getPixelsSize().getWidth() - childWidget->getPixelsSize().getWidth() ) / 2.f;
|
|
|
|
EXPECT_NEAR( childWidget->getLayoutPixelsMargin().Left, expectedMarginX, 1.f );
|
|
EXPECT_NEAR( childWidget->getLayoutPixelsMargin().Right, expectedMarginX, 1.f );
|
|
|
|
childWidget->setLayoutMarginAuto( false, false, true, true );
|
|
sceneNode->updateDirtyLayouts();
|
|
|
|
Float expectedMarginY =
|
|
( contWidget->getPixelsSize().getHeight() - childWidget->getPixelsSize().getHeight() ) /
|
|
2.f;
|
|
|
|
EXPECT_NEAR( childWidget->getLayoutPixelsMargin().Top, expectedMarginY, 1.f );
|
|
EXPECT_NEAR( childWidget->getLayoutPixelsMargin().Bottom, expectedMarginY, 1.f );
|
|
EXPECT_FALSE( childWidget->hasLayoutMarginLeftAuto() );
|
|
EXPECT_FALSE( childWidget->hasLayoutMarginRightAuto() );
|
|
EXPECT_TRUE( childWidget->hasLayoutMarginTopAuto() );
|
|
EXPECT_TRUE( childWidget->hasLayoutMarginBottomAuto() );
|
|
|
|
Engine::destroySingleton();
|
|
}
|
|
|
|
UTEST( UILayout, listStyleTypeDecimal ) {
|
|
init_ui_test();
|
|
auto* sceneNode = SceneManager::instance()->getUISceneNode();
|
|
sceneNode->loadLayoutFromString( R"html(
|
|
<html>
|
|
<ol>
|
|
<li id="li1" style="list-style-type: decimal;">First item</li>
|
|
<li id="li2" style="list-style-type: decimal;">Second item</li>
|
|
<li id="li3" style="list-style-type: decimal;">Third item</li>
|
|
</ol>
|
|
</html>
|
|
)html" );
|
|
|
|
sceneNode->updateDirtyLayouts();
|
|
|
|
const auto* propDef = StyleSheetSpecification::instance()->getProperty( "list-style-type" );
|
|
ASSERT_TRUE( propDef != nullptr );
|
|
|
|
auto* li1 = sceneNode->getRoot()->find( "li1" )->asType<UIRichText>();
|
|
auto* li2 = sceneNode->getRoot()->find( "li2" )->asType<UIRichText>();
|
|
auto* li3 = sceneNode->getRoot()->find( "li3" )->asType<UIRichText>();
|
|
|
|
ASSERT_TRUE( li1 != nullptr );
|
|
ASSERT_TRUE( li2 != nullptr );
|
|
ASSERT_TRUE( li3 != nullptr );
|
|
|
|
EXPECT_TRUE( li1->getPropertyString( propDef ) == "decimal" );
|
|
EXPECT_TRUE( li2->getPropertyString( propDef ) == "decimal" );
|
|
EXPECT_TRUE( li3->getPropertyString( propDef ) == "decimal" );
|
|
|
|
Engine::destroySingleton();
|
|
}
|
|
|
|
UTEST( UILayout, textListMarkerTracksInheritedColor ) {
|
|
FontStyleConfig inheritedStyle;
|
|
inheritedStyle.FontColor = Color( 224, 230, 237 );
|
|
inheritedStyle.CharacterSize = 19;
|
|
|
|
Text marker;
|
|
marker.setString( "1." );
|
|
UIHTMLListStyle::syncTextMarkerColor( marker, inheritedStyle.FontColor );
|
|
|
|
EXPECT_TRUE( marker.getFillColor() == inheritedStyle.FontColor );
|
|
}
|
|
|
|
UTEST( UILayout, textListMarkerPositionIsPixelAligned ) {
|
|
const Vector2f markerPos = UIHTMLListStyle::getTextMarkerPosition(
|
|
{ 100.75f, 40.5f }, Rectf( 0.25f, 1.75f, 0.f, 0.f ), 12.4f, 17.f );
|
|
|
|
EXPECT_TRUE( markerPos == markerPos.floor() );
|
|
}
|
|
|
|
UTEST( UILayout, listStyleTypeDisc ) {
|
|
init_ui_test();
|
|
auto* sceneNode = SceneManager::instance()->getUISceneNode();
|
|
sceneNode->loadLayoutFromString( R"html(
|
|
<html>
|
|
<ul>
|
|
<li id="li1" style="list-style-type: disc;">Bullet item</li>
|
|
</ul>
|
|
</html>
|
|
)html" );
|
|
|
|
sceneNode->updateDirtyLayouts();
|
|
|
|
const auto* propDef = StyleSheetSpecification::instance()->getProperty( "list-style-type" );
|
|
ASSERT_TRUE( propDef != nullptr );
|
|
|
|
auto* li1 = sceneNode->getRoot()->find( "li1" )->asType<UIRichText>();
|
|
ASSERT_TRUE( li1 != nullptr );
|
|
|
|
EXPECT_TRUE( li1->getPropertyString( propDef ) == "disc" );
|
|
|
|
Engine::destroySingleton();
|
|
}
|
|
|
|
UTEST( UILayout, listStyleTypeDisclosure ) {
|
|
init_ui_test();
|
|
auto* sceneNode = SceneManager::instance()->getUISceneNode();
|
|
sceneNode->loadLayoutFromString( R"html(
|
|
<html>
|
|
<ul>
|
|
<li id="li1" style="list-style-type: disclosure-closed;">Closed</li>
|
|
<li id="li2" style="list-style-type: disclosure-open;">Open</li>
|
|
</ul>
|
|
</html>
|
|
)html" );
|
|
|
|
sceneNode->updateDirtyLayouts();
|
|
|
|
const auto* propDef = StyleSheetSpecification::instance()->getProperty( "list-style-type" );
|
|
ASSERT_TRUE( propDef != nullptr );
|
|
|
|
auto* li1 = sceneNode->getRoot()->find( "li1" )->asType<UIRichText>();
|
|
auto* li2 = sceneNode->getRoot()->find( "li2" )->asType<UIRichText>();
|
|
ASSERT_TRUE( li1 != nullptr );
|
|
ASSERT_TRUE( li2 != nullptr );
|
|
|
|
EXPECT_TRUE( li1->getPropertyString( propDef ) == "disclosure-closed" );
|
|
EXPECT_TRUE( li2->getPropertyString( propDef ) == "disclosure-open" );
|
|
|
|
Engine::destroySingleton();
|
|
}
|
|
|
|
UTEST( UILayout, listStyleShorthand ) {
|
|
init_ui_test();
|
|
auto* sceneNode = SceneManager::instance()->getUISceneNode();
|
|
sceneNode->loadLayoutFromString( R"html(
|
|
<html>
|
|
<ol>
|
|
<li id="li1" style="list-style: decimal outside;">First</li>
|
|
<li id="li2" style="list-style: lower-alpha inside;">Second</li>
|
|
<li id="li3" style="list-style: none;">Third</li>
|
|
</ol>
|
|
<ul>
|
|
<li id="li4" style="list-style: disc;">Bullet</li>
|
|
<li id="li5" style="list-style: square outside;">Square</li>
|
|
<li id="li6" style="list-style: circle;">Circle</li>
|
|
</ul>
|
|
</html>
|
|
)html" );
|
|
|
|
sceneNode->updateDirtyLayouts();
|
|
|
|
const auto* typeDef = StyleSheetSpecification::instance()->getProperty( "list-style-type" );
|
|
const auto* posDef = StyleSheetSpecification::instance()->getProperty( "list-style-position" );
|
|
|
|
for ( const char* id : { "li1", "li2", "li3", "li4", "li5", "li6" } ) {
|
|
auto* li = sceneNode->getRoot()->find( id )->asType<UIWidget>();
|
|
ASSERT_TRUE( li != nullptr );
|
|
EXPECT_TRUE( li->isType( UI_TYPE_HTML_LIST_ITEM ) );
|
|
}
|
|
|
|
EXPECT_TRUE( sceneNode->getRoot()->find( "li1" )->asType<UIRichText>()->getPropertyString(
|
|
typeDef ) == "decimal" );
|
|
EXPECT_TRUE( sceneNode->getRoot()->find( "li1" )->asType<UIRichText>()->getPropertyString(
|
|
posDef ) == "outside" );
|
|
|
|
EXPECT_TRUE( sceneNode->getRoot()->find( "li2" )->asType<UIRichText>()->getPropertyString(
|
|
typeDef ) == "lower-alpha" );
|
|
EXPECT_TRUE( sceneNode->getRoot()->find( "li2" )->asType<UIRichText>()->getPropertyString(
|
|
posDef ) == "inside" );
|
|
|
|
EXPECT_TRUE( sceneNode->getRoot()->find( "li3" )->asType<UIRichText>()->getPropertyString(
|
|
typeDef ) == "none" );
|
|
|
|
EXPECT_TRUE( sceneNode->getRoot()->find( "li4" )->asType<UIRichText>()->getPropertyString(
|
|
typeDef ) == "disc" );
|
|
EXPECT_TRUE( sceneNode->getRoot()->find( "li5" )->asType<UIRichText>()->getPropertyString(
|
|
typeDef ) == "square" );
|
|
EXPECT_TRUE( sceneNode->getRoot()->find( "li5" )->asType<UIRichText>()->getPropertyString(
|
|
posDef ) == "outside" );
|
|
EXPECT_TRUE( sceneNode->getRoot()->find( "li6" )->asType<UIRichText>()->getPropertyString(
|
|
typeDef ) == "circle" );
|
|
|
|
Engine::destroySingleton();
|
|
}
|
|
|
|
UTEST( UILayout, listStyleInheritanceFromUl ) {
|
|
init_ui_test();
|
|
auto* sceneNode = SceneManager::instance()->getUISceneNode();
|
|
sceneNode->loadLayoutFromString( R"html(
|
|
<html>
|
|
<head>
|
|
<style>
|
|
ul.a { list-style-type: circle; }
|
|
ul.b { list-style-type: disc; }
|
|
ul.c { list-style-type: square; }
|
|
ol.d { list-style-type: decimal; }
|
|
ol.h { list-style-type: upper-roman; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<ol class="h">
|
|
<li id="h1">Coffee</li>
|
|
</ol>
|
|
<ul class="a">
|
|
<li id="a1">Coffee</li>
|
|
</ul>
|
|
<ul class="b">
|
|
<li id="b1">Coffee</li>
|
|
</ul>
|
|
<ul class="c">
|
|
<li id="c1">Coffee</li>
|
|
</ul>
|
|
<ol class="d">
|
|
<li id="d1">Coffee</li>
|
|
</ol>
|
|
</body>
|
|
</html>
|
|
)html" );
|
|
|
|
sceneNode->updateDirtyLayouts();
|
|
|
|
const auto* typeDef = StyleSheetSpecification::instance()->getProperty( "list-style-type" );
|
|
|
|
EXPECT_TRUE( sceneNode->getRoot()->find( "h1" )->asType<UIRichText>()->getPropertyString(
|
|
typeDef ) == "upper-roman" );
|
|
EXPECT_TRUE( sceneNode->getRoot()->find( "a1" )->asType<UIRichText>()->getPropertyString(
|
|
typeDef ) == "circle" );
|
|
EXPECT_TRUE( sceneNode->getRoot()->find( "b1" )->asType<UIRichText>()->getPropertyString(
|
|
typeDef ) == "disc" );
|
|
EXPECT_TRUE( sceneNode->getRoot()->find( "c1" )->asType<UIRichText>()->getPropertyString(
|
|
typeDef ) == "square" );
|
|
EXPECT_TRUE( sceneNode->getRoot()->find( "d1" )->asType<UIRichText>()->getPropertyString(
|
|
typeDef ) == "decimal" );
|
|
|
|
Engine::destroySingleton();
|
|
}
|
|
|
|
UTEST( UIHTMLDetails, lobstersInlineBlockCachesWidth ) {
|
|
Engine::instance()->createWindow( WindowSettings( 424, 184, "HTML Details Lobsters Test",
|
|
WindowStyle::Default, WindowBackend::Default,
|
|
32, {}, 1, false, true ) );
|
|
FileSystem::changeWorkingDirectory( Sys::getProcessPath() );
|
|
|
|
FontTrueType* font = FontTrueType::New( "NotoSans-Regular" ).get();
|
|
font->loadFromFile( "../assets/fonts/NotoSans-Regular.ttf" );
|
|
FontFamily::loadFromRegular( font );
|
|
|
|
UISceneNode* sceneNode = UISceneNode::New();
|
|
SceneManager::instance()->add( sceneNode );
|
|
UI::UIThemeManager* themeManager = sceneNode->getUIThemeManager();
|
|
themeManager->setDefaultFont( font );
|
|
sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( R"html(
|
|
<!doctype html>
|
|
<html>
|
|
<head></head>
|
|
<body>
|
|
<div id="inside">
|
|
<ol class="stories list">
|
|
<li id="story_4g74mw" class="story">
|
|
<div class="story_liner h-entry">
|
|
<div class="voters">
|
|
<a class="upvoter" href="/login">109</a>
|
|
</div>
|
|
<div class="details">
|
|
<span role="heading" aria-level="1" class="link h-cite u-repost-of">
|
|
<a class="u-url" href="https://ratfactor.com/ascetic-computing">Ascetic Computing</a>
|
|
</span>
|
|
<span class="tags">
|
|
<a class="tag tag_practices" href="/t/practices">practices</a>
|
|
</span>
|
|
<a class="domain" href="/domains/ratfactor.com">ratfactor.com</a>
|
|
<div class="byline">
|
|
<a tabindex="-1" aria-hidden="true" href="/~jbauer"><img class="avatar" alt="jbauer avatar" src="/avatars/jbauer-16.png" width="16" height="16"></a>
|
|
<span> via </span>
|
|
<a class="u-author h-card" href="/~jbauer">jbauer</a>
|
|
<time>20 hours ago</time>
|
|
<span aria-hidden="true"> | </span>
|
|
<details class="caches" name="caches">
|
|
<summary>caches</summary>
|
|
<ul>
|
|
<li><a href="https://web.archive.org/">Archive.org</a></li>
|
|
<li><a href="https://ghostarchive.org/">Ghostarchive</a></li>
|
|
</ul>
|
|
</details>
|
|
<span class="comments_label">
|
|
<span aria-hidden="true"> | </span>
|
|
<a role="heading" aria-level="2" href="/s/4g74mw/ascetic_computing">17 comments</a>
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</li>
|
|
</ol>
|
|
</div>
|
|
</body>
|
|
</html>
|
|
)html" ) );
|
|
sceneNode->updateDirtyLayouts();
|
|
|
|
sceneNode->combineStyleSheet( R"css(
|
|
body, textarea, input, button {
|
|
font-family: "helvetica neue", arial, sans-serif;
|
|
line-height: 1.45em;
|
|
}
|
|
ol.stories {
|
|
padding: 0;
|
|
list-style: none;
|
|
margin: 0;
|
|
}
|
|
div.voters {
|
|
float: left;
|
|
text-align: center;
|
|
width: 40px;
|
|
}
|
|
li.story {
|
|
clear: both;
|
|
}
|
|
ol.stories li.story div.story_liner {
|
|
padding-top: 0.25em;
|
|
padding-bottom: 0.25em;
|
|
word-break: break-word;
|
|
}
|
|
li div.details {
|
|
padding-top: 0.1em;
|
|
margin-left: 32px;
|
|
}
|
|
li .link {
|
|
font-weight: bold;
|
|
vertical-align: middle;
|
|
}
|
|
li .link a {
|
|
text-decoration: none;
|
|
}
|
|
li.story a.tag {
|
|
vertical-align: middle;
|
|
}
|
|
li .tags {
|
|
margin-right: 0.25em;
|
|
}
|
|
li .domain {
|
|
font-style: italic;
|
|
text-decoration: none;
|
|
vertical-align: middle;
|
|
}
|
|
img.avatar {
|
|
border-radius: 8px;
|
|
height: 16px;
|
|
margin-bottom: 2px;
|
|
margin-right: 2px;
|
|
vertical-align: middle;
|
|
width: 16px;
|
|
}
|
|
li.story .byline {
|
|
margin-top: 1px;
|
|
}
|
|
.caches {
|
|
display: inline-block;
|
|
position: relative;
|
|
}
|
|
.caches summary {
|
|
list-style: none;
|
|
}
|
|
.caches ul {
|
|
position: absolute;
|
|
white-space: nowrap;
|
|
list-style: none;
|
|
padding: 0;
|
|
z-index: 1;
|
|
}
|
|
.caches a {
|
|
text-decoration: none;
|
|
display: block;
|
|
padding: 3px 7px;
|
|
}
|
|
@media only screen and (max-width: 480px) {
|
|
div#inside {
|
|
margin: 0.5rem;
|
|
}
|
|
ol.stories {
|
|
margin: 0 0 0 -0.5rem;
|
|
padding-left: 0;
|
|
}
|
|
div.voters {
|
|
margin-left: 0.25em;
|
|
margin-top: 0px;
|
|
width: 30px;
|
|
}
|
|
ol.stories.list {
|
|
margin-top: 0;
|
|
}
|
|
ol.stories.list li.story {
|
|
display: table;
|
|
}
|
|
ol.stories.list li.story div.story_liner {
|
|
display: table-cell;
|
|
padding-top: 0.5em;
|
|
padding-bottom: 0.75em;
|
|
width: 100%;
|
|
}
|
|
li div.details {
|
|
margin: 0 0 0 36px;
|
|
}
|
|
}
|
|
)css" );
|
|
sceneNode->updateDirtyLayouts();
|
|
|
|
auto* caches = sceneNode->getRoot()->querySelector( ".caches" )->asType<UIHTMLDetails>();
|
|
auto* comments = sceneNode->getRoot()->querySelector( ".comments_label" );
|
|
auto* byline = sceneNode->getRoot()->querySelector( ".byline" );
|
|
|
|
EXPECT_LT( caches->getPixelsSize().getWidth(), byline->getPixelsSize().getWidth() * 0.5f );
|
|
EXPECT_NEAR( caches->getPixelsPosition().y, comments->getPixelsPosition().y, 1.f );
|
|
|
|
Engine::destroySingleton();
|
|
}
|
|
|
|
UTEST( UIBorder, renderingVariations ) {
|
|
auto win = Engine::instance()->createWindow(
|
|
WindowSettings( 1024, 653, "Border Rendering Test", VisualTestWindowStyle,
|
|
WindowBackend::Default, 32, {}, 1, false, true ),
|
|
ContextSettings( false, 0, 0, GLv_default, true, false ) );
|
|
FileSystem::changeWorkingDirectory( Sys::getProcessPath() );
|
|
|
|
FontTrueType* font = FontTrueType::New( "NotoSans-Regular" ).get();
|
|
font->loadFromFile( "../assets/fonts/NotoSans-Regular.ttf" );
|
|
ASSERT_TRUE( font != nullptr && font->loaded() );
|
|
FontFamily::loadFromRegular( font );
|
|
|
|
UISceneNode* sceneNode = UISceneNode::New();
|
|
SceneManager::instance()->add( sceneNode );
|
|
UI::UIThemeManager* themeManager = sceneNode->getUIThemeManager();
|
|
themeManager->setDefaultFont( font );
|
|
sceneNode->setURI( "file://" + Sys::getProcessPath() + "assets/html/" );
|
|
std::string html;
|
|
FileSystem::fileGet( "assets/html/border_tests.html", html );
|
|
sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( html ) );
|
|
win->setClearColor( Color::White );
|
|
|
|
win->getInput()->update();
|
|
SceneManager::instance()->update();
|
|
|
|
win->clear();
|
|
SceneManager::instance()->draw();
|
|
win->display();
|
|
|
|
compareImages( utest_state, utest_result, win, "eepp-ui-border-rendering", "html", 12 );
|
|
|
|
Engine::destroySingleton();
|
|
}
|
|
|
|
UTEST( UIBorder, cssSideStyles ) {
|
|
init_ui_test();
|
|
auto* sceneNode = SceneManager::instance()->getUISceneNode();
|
|
sceneNode->loadLayoutFromString( R"html(
|
|
<html><body>
|
|
<div id="styled" style="width: 100px; height: 40px;
|
|
border-top: 1px dotted gray; border-right: 2px dashed red;
|
|
border-bottom: 3px solid blue; border-left: none;"></div>
|
|
<div id="initial" style="width: 100px; height: 40px; border-top-width: 5px;"></div>
|
|
</body></html>
|
|
)html" );
|
|
sceneNode->updateDirtyLayouts();
|
|
|
|
auto* styled = sceneNode->getRoot()->find( "styled" )->asType<UIWidget>();
|
|
ASSERT_TRUE( styled != nullptr );
|
|
const Borders& borders = styled->getBorder()->getBorders();
|
|
EXPECT_EQ( borders.top.style, BorderStyle::Dotted );
|
|
EXPECT_EQ( borders.right.style, BorderStyle::Dashed );
|
|
EXPECT_EQ( borders.bottom.style, BorderStyle::Solid );
|
|
EXPECT_EQ( borders.left.style, BorderStyle::None );
|
|
EXPECT_EQ( borders.top.width, 1 );
|
|
EXPECT_EQ( borders.right.width, 2 );
|
|
EXPECT_EQ( borders.bottom.width, 3 );
|
|
EXPECT_EQ( borders.left.width, 0 );
|
|
|
|
auto* initial = sceneNode->getRoot()->find( "initial" )->asType<UIWidget>();
|
|
ASSERT_TRUE( initial != nullptr );
|
|
const Borders& initialBorders = initial->getBorder()->getBorders();
|
|
EXPECT_EQ( initialBorders.top.style, BorderStyle::None );
|
|
EXPECT_EQ( initialBorders.top.width, 5 );
|
|
|
|
Engine::destroySingleton();
|
|
}
|
|
|
|
UTEST( UIBorder, renderingVariations2 ) {
|
|
auto win = Engine::instance()->createWindow(
|
|
WindowSettings( 1024, 653, "Border Rendering Test 2", VisualTestWindowStyle,
|
|
WindowBackend::Default, 32, {}, 1, false, true ),
|
|
ContextSettings( false, 0, 0, GLv_default, true, false ) );
|
|
FileSystem::changeWorkingDirectory( Sys::getProcessPath() );
|
|
|
|
FontTrueType* font = FontTrueType::New( "NotoSans-Regular" ).get();
|
|
font->loadFromFile( "../assets/fonts/NotoSans-Regular.ttf" );
|
|
ASSERT_TRUE( font != nullptr && font->loaded() );
|
|
FontFamily::loadFromRegular( font );
|
|
|
|
UISceneNode* sceneNode = UISceneNode::New();
|
|
SceneManager::instance()->add( sceneNode );
|
|
UI::UIThemeManager* themeManager = sceneNode->getUIThemeManager();
|
|
themeManager->setDefaultFont( font );
|
|
sceneNode->setURI( "file://" + Sys::getProcessPath() + "assets/html/" );
|
|
std::string html;
|
|
FileSystem::fileGet( "assets/html/border_tests_2.html", html );
|
|
sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( html ) );
|
|
win->setClearColor( Color::White );
|
|
|
|
win->getInput()->update();
|
|
SceneManager::instance()->update();
|
|
|
|
win->clear();
|
|
SceneManager::instance()->draw();
|
|
win->display();
|
|
|
|
compareImages( utest_state, utest_result, win, "eepp-ui-border-rendering-2", "html", 12 );
|
|
|
|
Engine::destroySingleton();
|
|
}
|
|
|
|
static UISceneNode* init_test_inline_block() {
|
|
FileSystem::changeWorkingDirectory( Sys::getProcessPath() );
|
|
FontTrueType* font = FontTrueType::New( "NotoSans-Regular" ).get();
|
|
font->loadFromFile( "../assets/fonts/NotoSans-Regular.ttf" );
|
|
FontFamily::loadFromRegular( font );
|
|
FontTrueType* monoFont = FontTrueType::New( "monospace" ).get();
|
|
monoFont->loadFromFile( "../assets/fonts/NotoSans-Regular.ttf" );
|
|
UISceneNode* sceneNode = UISceneNode::New();
|
|
SceneManager::instance()->add( sceneNode );
|
|
SceneManager::instance()->setCurrentUISceneNode( sceneNode );
|
|
UIThemeManager* themeManager = sceneNode->getUIThemeManager();
|
|
themeManager->setDefaultFont( font );
|
|
themeManager->applyDefaultTheme( sceneNode->getRoot() );
|
|
return sceneNode;
|
|
}
|
|
|
|
UTEST( UIHTML, BodyViewportMinimumHeightUsesSceneViewport ) {
|
|
Engine::instance()->createWindow( WindowSettings( 1024, 768, "Body Viewport Height Test",
|
|
WindowStyle::Default, WindowBackend::Default,
|
|
32, {}, 1, false, true ),
|
|
ContextSettings( false, 0, 0, GLv_default, true, false ) );
|
|
|
|
UISceneNode* sceneNode = init_test_inline_block();
|
|
sceneNode->setPixelsSize( 800, 3000 );
|
|
sceneNode->setViewportPixelsSize( Sizef( 800, 600 ) );
|
|
|
|
const std::string html = R"html(
|
|
<!DOCTYPE html>
|
|
<html>
|
|
<head><style>body { margin: 0; min-height: 100vh; }</style></head>
|
|
<body><div style="height: 10px;"></div></body>
|
|
</html>
|
|
)html";
|
|
|
|
sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( html ) );
|
|
sceneNode->update( Seconds( 1 ) );
|
|
sceneNode->updateDirtyLayouts();
|
|
|
|
auto bodyNode = sceneNode->getRoot()->findByType( UI_TYPE_HTML_BODY );
|
|
ASSERT_TRUE( bodyNode != nullptr );
|
|
auto body = bodyNode->asType<UIHTMLBody>();
|
|
body->applyProperty( StyleSheetProperty( "min-height", "100vh" ) );
|
|
body->updateLayout();
|
|
EXPECT_NEAR( bodyNode->getPixelsSize().getHeight(), 600.f, 1.f );
|
|
EXPECT_EQ( sceneNode->getPixelsSize().getWidth(), 800 );
|
|
EXPECT_EQ( sceneNode->getPixelsSize().getHeight(), 3000 );
|
|
|
|
Engine::destroySingleton();
|
|
}
|
|
|
|
UTEST( UIHTML, DuckDuckGoHomepageAbsoluteContentDoesNotGrowBody ) {
|
|
auto win = Engine::instance()->createWindow(
|
|
WindowSettings( 1280, 720, "DuckDuckGo homepage body height", WindowStyle::Default,
|
|
WindowBackend::Default, 32, {}, 1, false, true ),
|
|
ContextSettings( false, 0, 0, GLv_default, true, false ) );
|
|
UISceneNode* sceneNode = init_test_inline_block();
|
|
|
|
auto* vbox = sceneNode->loadLayoutFromString( R"xml(
|
|
<vbox layout_width="match_parent" layout_height="match_parent">
|
|
<hbox layout_width="match_parent" layout_height="wrap_content">
|
|
<PushButton lw="26dp" id="backbtn" text="Back" />
|
|
<PushButton lw="26dp" id="fwdbtn" text="Forward" />
|
|
<PushButton lw="26dp" id="refreshbtn" text="Refresh" />
|
|
<TextInput id="url_bar" layout_width="0" layout_weight="1" />
|
|
</hbox>
|
|
<WebView id="webview" layout_width="match_parent" layout_height="0"
|
|
layout_weight="1" />
|
|
</vbox>
|
|
)xml" );
|
|
ASSERT_TRUE( vbox != nullptr );
|
|
auto* webView = vbox->find( "webview" )->asType<UIWebView>();
|
|
ASSERT_TRUE( webView != nullptr );
|
|
|
|
auto* documentScene = webView->getDocumentSceneNode();
|
|
bool navigationCompleted = false;
|
|
webView->onNavigationCompleted(
|
|
[&navigationCompleted]( const URI& ) { navigationCompleted = true; } );
|
|
webView->loadURI( URI( "assets/html/ddg_html.html" ) );
|
|
|
|
for ( int i = 0; i < 300; ++i ) {
|
|
win->getInput()->update();
|
|
SceneManager::instance()->update( Seconds( 1.f / 60.f ) );
|
|
Sys::sleep( Milliseconds( 1 ) );
|
|
}
|
|
ASSERT_TRUE( navigationCompleted );
|
|
|
|
auto* body = documentScene->getRoot()->findByType( UI_TYPE_HTML_BODY )->asType<UIWidget>();
|
|
auto* content =
|
|
documentScene->getRoot()->find( "content_wrapper_homepage" )->asType<UIWidget>();
|
|
ASSERT_TRUE( body != nullptr );
|
|
ASSERT_TRUE( content != nullptr );
|
|
EXPECT_NEAR( body->getPixelsSize().getHeight(),
|
|
documentScene->getViewportPixelsSize().getHeight(), 2.f );
|
|
EXPECT_NEAR( webView->getDocumentContainer()->getPixelsSize().getHeight(),
|
|
documentScene->getViewportPixelsSize().getHeight(), 2.f );
|
|
EXPECT_NEAR( content->getPixelsPosition().y,
|
|
documentScene->getLayoutViewportPixelsSize().getHeight() * 0.24f, 2.f );
|
|
EXPECT_LT( content->getPixelsPosition().y, body->getPixelsSize().getHeight() );
|
|
EXPECT_LT( content->getPixelsPosition().y + content->getPixelsSize().getHeight(),
|
|
body->getPixelsSize().getHeight() );
|
|
EXPECT_FALSE( webView->getHorizontalScrollBar()->isVisible() );
|
|
EXPECT_FALSE( webView->getVerticalScrollBar()->isVisible() );
|
|
|
|
Engine::destroySingleton();
|
|
}
|
|
|
|
UTEST( UIHTML, InlineBlock ) {
|
|
Engine::instance()->createWindow( WindowSettings( 1024, 768, "Inline Block Test",
|
|
WindowStyle::Default, WindowBackend::Default,
|
|
32, {}, 1, false, true ),
|
|
ContextSettings( false, 0, 0, GLv_default, true, false ) );
|
|
|
|
UISceneNode* sceneNode = init_test_inline_block();
|
|
|
|
const std::string html = R"html(
|
|
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<style>
|
|
ul > li {
|
|
display: inline-block;
|
|
border: 1px solid red;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<ul class="flat-list buttons">
|
|
<li><a href="#">6 comments</a></li>
|
|
<li><a class="post-sharing-button" href="#">share</a></li>
|
|
<li><a href="#">save</a></li>
|
|
<li><span><a href="#">hide</a></span></li>
|
|
<li><a href="#">report</a></li>
|
|
<li><a href="#">crosspost</a></li>
|
|
</ul>
|
|
</body>
|
|
</html>
|
|
)html";
|
|
|
|
sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( html ) );
|
|
|
|
auto ul = sceneNode->getRoot()->findByTag( "ul" );
|
|
ASSERT_TRUE( ul != nullptr );
|
|
|
|
// Force layout update
|
|
sceneNode->update( Seconds( 1 ) );
|
|
|
|
auto lis = ul->findAllByTag( "li" );
|
|
EXPECT_EQ( lis.size(), (size_t)6 );
|
|
|
|
for ( auto li : lis ) {
|
|
EXPECT_EQ( li->asType<UIHTMLWidget>()->getDisplay(), CSSDisplay::InlineBlock );
|
|
EXPECT_EQ( li->getLayoutWidthPolicy(), SizePolicy::WrapContent );
|
|
EXPECT_GT( li->getPixelsSize().getWidth(), 0 );
|
|
EXPECT_LT( li->getPixelsSize().getWidth(), ul->getPixelsSize().getWidth() );
|
|
EXPECT_GT( li->getPixelsSize().getHeight(), 0 );
|
|
}
|
|
|
|
// Check if they are on the same line (inline-block)
|
|
if ( lis.size() >= 2 ) {
|
|
EXPECT_EQ( lis[0]->getPixelsPosition().y, lis[1]->getPixelsPosition().y );
|
|
EXPECT_LT( lis[0]->getPixelsPosition().x, lis[1]->getPixelsPosition().x );
|
|
}
|
|
|
|
Engine::destroySingleton();
|
|
}
|
|
|
|
UTEST( UIHTML, InlineAnchorsInsideInlineBlockSiblingsKeepDocumentOrder ) {
|
|
init_ui_test();
|
|
UISceneNode* sceneNode = SceneManager::instance()->getUISceneNode();
|
|
sceneNode->setThreadPool( ThreadPool::createShared( 1 ) );
|
|
sceneNode->setURI( "file://" + Sys::getProcessPath() + "assets/html/" );
|
|
|
|
sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( R"html(
|
|
<html><head>
|
|
<link rel="stylesheet" href="reddit_inline_order_deferred.css" defer="25" />
|
|
</head><body>
|
|
<div class="entry">
|
|
<p id="title"><a id="post-title">Low-level coding dataset</a>
|
|
<span id="domain">(<a>self.cpp</a>)</span></p>
|
|
<ul class="buttons" id="comment-buttons">
|
|
<span class="float-marker"></span>
|
|
<li><a>permalink</a></li><li><a>embed</a></li><li><a>save</a></li>
|
|
<li><a>report</a></li><li><a>reply</a></li>
|
|
</ul>
|
|
</div>
|
|
</body></html>
|
|
)html" ) );
|
|
|
|
UIWidget* buttons = nullptr;
|
|
for ( int i = 0; i < 200; ++i ) {
|
|
SceneManager::instance()->update();
|
|
buttons = sceneNode->getRoot()->find( "comment-buttons" )->asType<UIWidget>();
|
|
if ( buttons ) {
|
|
auto items = buttons->findAllByTag( "li" );
|
|
if ( !items.empty() &&
|
|
items.front()->asType<UIHTMLWidget>()->getDisplay() == CSSDisplay::InlineBlock )
|
|
break;
|
|
}
|
|
Sys::sleep( Milliseconds( 1 ) );
|
|
}
|
|
|
|
auto* postTitle = sceneNode->getRoot()->find( "post-title" )->asType<UIWidget>();
|
|
auto* domain = sceneNode->getRoot()->find( "domain" )->asType<UIWidget>();
|
|
ASSERT_TRUE( postTitle != nullptr );
|
|
ASSERT_TRUE( domain != nullptr );
|
|
ASSERT_TRUE( buttons != nullptr );
|
|
EXPECT_GE( domain->getPixelsPosition().x,
|
|
postTitle->getPixelsPosition().x + postTitle->getPixelsSize().getWidth() );
|
|
|
|
auto listItems = buttons->findAllByTag( "li" );
|
|
ASSERT_EQ( listItems.size(), (size_t)5 );
|
|
Float previousRight = listItems.front()->getPixelsPosition().x;
|
|
for ( auto* listItem : listItems ) {
|
|
EXPECT_GE( listItem->getPixelsPosition().x, previousRight );
|
|
EXPECT_GT( listItem->getPixelsSize().getWidth(), 0.f );
|
|
previousRight = listItem->getPixelsPosition().x + listItem->getPixelsSize().getWidth();
|
|
}
|
|
|
|
Engine::destroySingleton();
|
|
}
|
|
|
|
UTEST( UIHTML, StyleSheetTraversalBoundaries ) {
|
|
Engine::instance()->createWindow( WindowSettings( 1024, 768, "CSS Traversal Test",
|
|
WindowStyle::Default, WindowBackend::Default,
|
|
32, {}, 1, false, true ),
|
|
ContextSettings( false, 0, 0, GLv_default, true, false ) );
|
|
|
|
UISceneNode* sceneNode = init_test_inline_block();
|
|
const std::string html = R"html(
|
|
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<style>
|
|
.goog-inline-block { display: inline-block; }
|
|
* html .goog-inline-block { display: inline; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<a id="a" class="goog-inline-block"></a>
|
|
text
|
|
<div id="b"></div>
|
|
</body>
|
|
</html>
|
|
)html";
|
|
|
|
sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( html ) );
|
|
sceneNode->update( Seconds( 1 ) );
|
|
|
|
auto* htmlNode = sceneNode->getRoot()->findByType( UI_TYPE_HTML_HTML )->asType<UIWidget>();
|
|
auto* body = sceneNode->getRoot()->findByType( UI_TYPE_HTML_BODY )->asType<UIWidget>();
|
|
auto* a = sceneNode->getRoot()->find( "a" )->asType<UIHTMLWidget>();
|
|
auto* b = sceneNode->getRoot()->find( "b" )->asType<UIWidget>();
|
|
ASSERT_TRUE( htmlNode != nullptr );
|
|
ASSERT_TRUE( body != nullptr );
|
|
ASSERT_TRUE( a != nullptr );
|
|
ASSERT_TRUE( b != nullptr );
|
|
|
|
EXPECT_TRUE( htmlNode->getStyleSheetParentElement() == nullptr );
|
|
EXPECT_EQ( CSSDisplay::InlineBlock, a->getDisplay() );
|
|
EXPECT_TRUE( b->getStyleSheetPreviousSiblingElement() == a );
|
|
|
|
auto* nativeWidget = UIWidget::New();
|
|
nativeWidget->setParent( body );
|
|
auto* nativeChild = UIWidget::New();
|
|
nativeChild->setParent( nativeWidget );
|
|
EXPECT_TRUE( nativeWidget->getStyleSheetPreviousSiblingElement() == b );
|
|
EXPECT_TRUE( nativeChild->getStyleSheetParentElement() == nativeWidget );
|
|
|
|
Engine::destroySingleton();
|
|
}
|
|
|
|
UTEST( UIHTML, MarkdownViewLoadsBodyChildrenIntoNativeTree ) {
|
|
init_ui_test();
|
|
auto* sceneNode = SceneManager::instance()->getUISceneNode();
|
|
StyleSheetParser parser;
|
|
ASSERT_TRUE( parser.loadFromString( std::string_view{ "MarkdownView p { color: red; }" } ) );
|
|
sceneNode->setStyleSheet( parser.getStyleSheet() );
|
|
|
|
auto* markdownView = UIMarkdownView::New();
|
|
markdownView->setParent( sceneNode->getRoot() );
|
|
markdownView->loadFromString( "Paragraph" );
|
|
sceneNode->update( Seconds( 1 ) );
|
|
|
|
auto* paragraph = markdownView->findByTag( "p" );
|
|
ASSERT_TRUE( paragraph != nullptr );
|
|
ASSERT_TRUE( paragraph->isType( UI_TYPE_RICHTEXT ) );
|
|
EXPECT_TRUE( markdownView->findByType( UI_TYPE_HTML_HTML ) == nullptr );
|
|
EXPECT_TRUE( markdownView->findByType( UI_TYPE_HTML_BODY ) == nullptr );
|
|
EXPECT_TRUE( paragraph->getStyleSheetParentElement() == markdownView );
|
|
EXPECT_TRUE( Color::Red == paragraph->asType<UIRichText>()->getFontColor() );
|
|
|
|
Engine::destroySingleton();
|
|
}
|
|
|
|
UTEST( UIHTML, ScriptNestedInUnknownElementIsNotRendered ) {
|
|
init_ui_test();
|
|
auto* sceneNode = SceneManager::instance()->getUISceneNode();
|
|
|
|
sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( R"html(
|
|
<html><body><react-app id="app">
|
|
<script type="application/json">{"payload":"must-not-render"}</script>
|
|
<span id="visible">Visible</span>
|
|
</react-app></body></html>
|
|
)html" ) );
|
|
|
|
auto* app = sceneNode->getRoot()->find( "app" );
|
|
ASSERT_TRUE( app != nullptr );
|
|
EXPECT_TRUE( app->asType<UIWidget>()->findByTag( "script" ) == nullptr );
|
|
EXPECT_TRUE( app->find( "visible" ) != nullptr );
|
|
|
|
Engine::destroySingleton();
|
|
}
|
|
|
|
UTEST( UIHTML, UnsupportedBlockAtRulesDoNotLeakNestedRules ) {
|
|
StyleSheetParser parser;
|
|
ASSERT_TRUE( parser.loadFromString( std::string_view{
|
|
"@layer framework { .layered { color: red; } @supports (display: grid) { .nested { "
|
|
"display: grid; } } } .visible { color: blue; }" } ) );
|
|
|
|
EXPECT_TRUE( parser.getStyleSheet().findStyleFromSelectorName( ".layered" ).empty() );
|
|
EXPECT_TRUE( parser.getStyleSheet().findStyleFromSelectorName( ".nested" ).empty() );
|
|
EXPECT_EQ( 1u, parser.getStyleSheet().findStyleFromSelectorName( ".visible" ).size() );
|
|
}
|
|
|
|
UTEST( UIHTML, StyleSheetSiblingCombinators ) {
|
|
Engine::instance()->createWindow( WindowSettings( 1024, 768, "CSS Sibling Test",
|
|
WindowStyle::Default, WindowBackend::Default,
|
|
32, {}, 1, false, true ),
|
|
ContextSettings( false, 0, 0, GLv_default, true, false ) );
|
|
|
|
UISceneNode* sceneNode = init_test_inline_block();
|
|
const std::string html = R"html(
|
|
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<style>
|
|
#a + #b { background-color: red; }
|
|
#b + #a { background-color: blue; }
|
|
#a ~ #c { background-color: green; }
|
|
#c ~ #a { background-color: blue; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div id="a"></div>
|
|
text
|
|
<div id="b"></div>
|
|
<span></span>
|
|
<div id="c"></div>
|
|
</body>
|
|
</html>
|
|
)html";
|
|
|
|
sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( html ) );
|
|
sceneNode->update( Seconds( 1 ) );
|
|
|
|
auto* a = sceneNode->getRoot()->find( "a" )->asType<UIWidget>();
|
|
auto* b = sceneNode->getRoot()->find( "b" )->asType<UIWidget>();
|
|
auto* c = sceneNode->getRoot()->find( "c" )->asType<UIWidget>();
|
|
ASSERT_TRUE( a != nullptr );
|
|
ASSERT_TRUE( b != nullptr );
|
|
ASSERT_TRUE( c != nullptr );
|
|
|
|
EXPECT_TRUE( b->getBackgroundColor() == Color::Red );
|
|
EXPECT_TRUE( c->getBackgroundColor() == Color::Green );
|
|
EXPECT_TRUE( a->getBackgroundColor() != Color::Blue );
|
|
|
|
StyleSheetSelector directRelated( "#a:hover + #b" );
|
|
auto related = directRelated.getRelatedElements( b, false );
|
|
ASSERT_EQ( 1u, related.size() );
|
|
EXPECT_TRUE( related.front() == a );
|
|
|
|
StyleSheetSelector futureGeneralSibling( "#c:hover ~ #a" );
|
|
EXPECT_TRUE( futureGeneralSibling.getRelatedElements( a, false ).empty() );
|
|
|
|
StyleSheetSelector inverseSibling( "#b | #a" );
|
|
EXPECT_TRUE( inverseSibling.select( a ) );
|
|
EXPECT_FALSE( inverseSibling.select( b ) );
|
|
|
|
Engine::destroySingleton();
|
|
}
|
|
|
|
UTEST( UIHTML, UniversalSelectorRequirements ) {
|
|
Engine::instance()->createWindow( WindowSettings( 1024, 768, "Universal Selector Test",
|
|
WindowStyle::Default, WindowBackend::Default,
|
|
32, {}, 1, false, true ),
|
|
ContextSettings( false, 0, 0, GLv_default, true, false ) );
|
|
|
|
UISceneNode* sceneNode = init_test_inline_block();
|
|
const std::string html = R"html(
|
|
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<style>
|
|
*.foo { color: red; }
|
|
*[href] { color: green; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div id="class-match" class="foo">class match</div>
|
|
<div id="class-miss">class miss</div>
|
|
<a id="attribute-match" href="x">attribute match</a>
|
|
<a id="attribute-miss">attribute miss</a>
|
|
</body>
|
|
</html>
|
|
)html";
|
|
|
|
sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( html ) );
|
|
sceneNode->update( Seconds( 1 ) );
|
|
|
|
auto* classMatch = sceneNode->getRoot()->find( "class-match" )->asType<UIRichText>();
|
|
auto* classMiss = sceneNode->getRoot()->find( "class-miss" )->asType<UIRichText>();
|
|
auto* attributeMatch = sceneNode->getRoot()->find( "attribute-match" )->asType<UITextSpan>();
|
|
auto* attributeMiss = sceneNode->getRoot()->find( "attribute-miss" )->asType<UITextSpan>();
|
|
ASSERT_TRUE( classMatch != nullptr );
|
|
ASSERT_TRUE( classMiss != nullptr );
|
|
ASSERT_TRUE( attributeMatch != nullptr );
|
|
ASSERT_TRUE( attributeMiss != nullptr );
|
|
|
|
EXPECT_TRUE( classMatch->getFontColor() == Color::Red );
|
|
EXPECT_TRUE( classMiss->getFontColor() != Color::Red );
|
|
EXPECT_TRUE( attributeMatch->getFontColor() == Color::Green );
|
|
EXPECT_TRUE( attributeMiss->getFontColor() != Color::Green );
|
|
|
|
StyleSheetSelector universalClass( "*.foo" );
|
|
EXPECT_TRUE( universalClass.select( classMatch, false ) );
|
|
EXPECT_FALSE( universalClass.select( classMiss, false ) );
|
|
|
|
StyleSheetSelector universalAttribute( "*[href]" );
|
|
EXPECT_TRUE( universalAttribute.select( attributeMatch, false ) );
|
|
EXPECT_FALSE( universalAttribute.select( attributeMiss, false ) );
|
|
|
|
StyleSheetSelector pureUniversal( "*" );
|
|
EXPECT_TRUE( pureUniversal.select( classMiss, false ) );
|
|
EXPECT_TRUE( pureUniversal.select( attributeMiss, false ) );
|
|
|
|
Engine::destroySingleton();
|
|
}
|
|
|
|
UTEST( UIHTML, HashedSelectorMatchingAndClassMutation ) {
|
|
Engine::instance()->createWindow( WindowSettings( 1024, 768, "Hashed Selector Test",
|
|
WindowStyle::Default, WindowBackend::Default,
|
|
32, {}, 1, false, true ),
|
|
ContextSettings( false, 0, 0, GLv_default, true, false ) );
|
|
|
|
UISceneNode* sceneNode = init_test_inline_block();
|
|
sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( R"html(
|
|
<html><body><div id="target" class="foo bar"></div></body></html>
|
|
)html" ) );
|
|
|
|
auto* target = sceneNode->getRoot()->find( "target" )->asType<UIWidget>();
|
|
ASSERT_TRUE( target != nullptr );
|
|
EXPECT_EQ( String::hash( "div" ), target->getElementTagHash() );
|
|
EXPECT_TRUE( target->hasClassHashes( { String::hash( "foo" ) } ) );
|
|
EXPECT_TRUE( target->hasClassHashes( { String::hash( "bar" ) } ) );
|
|
EXPECT_EQ( 2u, target->getClassHashCount() );
|
|
|
|
EXPECT_TRUE( StyleSheetSelector( ".foo" ).select( target, false ) );
|
|
EXPECT_TRUE( StyleSheetSelector( ".foo.bar" ).select( target, false ) );
|
|
EXPECT_TRUE( StyleSheetSelector( "div.foo" ).select( target, false ) );
|
|
EXPECT_TRUE( StyleSheetSelector( "#target.foo" ).select( target, false ) );
|
|
EXPECT_FALSE( StyleSheetSelector( "span.foo" ).select( target, false ) );
|
|
EXPECT_FALSE( StyleSheetSelector( "#other.foo" ).select( target, false ) );
|
|
|
|
target->setClasses( { "alpha", "beta", "alpha" } );
|
|
EXPECT_EQ( 2u, target->getClassHashCount() );
|
|
EXPECT_TRUE( StyleSheetSelector( ".alpha.beta" ).select( target, false ) );
|
|
target->removeClass( "alpha" );
|
|
EXPECT_TRUE( StyleSheetSelector( ".alpha" ).select( target, false ) );
|
|
target->removeClass( "alpha" );
|
|
EXPECT_FALSE( StyleSheetSelector( ".alpha" ).select( target, false ) );
|
|
target->addClasses( { "gamma", "delta" } );
|
|
EXPECT_TRUE( StyleSheetSelector( ".beta.gamma.delta" ).select( target, false ) );
|
|
target->removeClasses( { "beta", "delta" } );
|
|
EXPECT_FALSE( target->hasClassHashes( { String::hash( "beta" ) } ) );
|
|
EXPECT_FALSE( target->hasClassHashes( { String::hash( "delta" ) } ) );
|
|
target->toggleClass( "gamma" );
|
|
EXPECT_FALSE( StyleSheetSelector( ".gamma" ).select( target, false ) );
|
|
target->toggleClass( "gamma" );
|
|
EXPECT_TRUE( StyleSheetSelector( ".gamma" ).select( target, false ) );
|
|
target->setClass( std::string( "moved" ) );
|
|
EXPECT_TRUE( StyleSheetSelector( ".moved" ).select( target, false ) );
|
|
target->resetClass();
|
|
EXPECT_EQ( 0u, target->getClassHashCount() );
|
|
|
|
target->setElementTag( "section" );
|
|
EXPECT_EQ( String::hash( "section" ), target->getElementTagHash() );
|
|
EXPECT_TRUE( StyleSheetSelector( "section" ).select( target, false ) );
|
|
EXPECT_FALSE( StyleSheetSelector( "div" ).select( target, false ) );
|
|
|
|
Engine::destroySingleton();
|
|
}
|
|
|
|
UTEST( UIHTML, SingleRuleSelectorMatching ) {
|
|
Engine::instance()->createWindow( WindowSettings( 1024, 768, "Single Rule Selector Test",
|
|
WindowStyle::Default, WindowBackend::Default,
|
|
32, {}, 1, false, true ),
|
|
ContextSettings( false, 0, 0, GLv_default, true, false ) );
|
|
|
|
UISceneNode* sceneNode = init_test_inline_block();
|
|
sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML(
|
|
R"html(<html><body><div id="target" class="foo bar"></div></body></html>)html" ) );
|
|
auto* target = sceneNode->getRoot()->find( "target" )->asType<UIWidget>();
|
|
ASSERT_TRUE( target != nullptr );
|
|
|
|
EXPECT_TRUE( StyleSheetSelector( ".foo" ).select( target, false ) );
|
|
EXPECT_TRUE( StyleSheetSelector( "#target" ).select( target, false ) );
|
|
EXPECT_TRUE( StyleSheetSelector( "div.foo" ).select( target, false ) );
|
|
EXPECT_TRUE( StyleSheetSelector( ".foo:hover" ).select( target, false ) );
|
|
EXPECT_FALSE( StyleSheetSelector( ".foo:hover" ).select( target, true ) );
|
|
|
|
Engine::destroySingleton();
|
|
}
|
|
|
|
UTEST( UIHTML, ClassIndexedStyleSheetCandidates ) {
|
|
Engine::instance()->createWindow( WindowSettings( 1024, 768, "Class Index Test",
|
|
WindowStyle::Default, WindowBackend::Default,
|
|
32, {}, 1, false, true ),
|
|
ContextSettings( false, 0, 0, GLv_default, true, false ) );
|
|
|
|
UISceneNode* sceneNode = init_test_inline_block();
|
|
sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML(
|
|
R"html(<html><body><div id="target" class="foo bar"></div></body></html>)html" ) );
|
|
auto* target = sceneNode->getRoot()->find( "target" )->asType<UIWidget>();
|
|
ASSERT_TRUE( target != nullptr );
|
|
|
|
StyleSheetParser parser;
|
|
ASSERT_TRUE( parser.loadFromString( std::string_view( R"css(
|
|
* { color: white; }
|
|
div { background-color: black; }
|
|
.foo { width: 10px; }
|
|
.foo.bar { height: 20px; }
|
|
div.foo { min-width: 5px; }
|
|
#target.foo { max-width: 50px; }
|
|
.unrelated { opacity: 0.5; }
|
|
)css" ) ) );
|
|
|
|
auto definition = parser.getStyleSheet().getElementStyles( target, false );
|
|
ASSERT_TRUE( definition != nullptr );
|
|
EXPECT_EQ( 6u, definition->getStyles().size() );
|
|
EXPECT_TRUE( std::none_of( definition->getStyles().begin(), definition->getStyles().end(),
|
|
[]( const StyleSheetStyle* style ) {
|
|
return style->getSelector().getName() == ".unrelated";
|
|
} ) );
|
|
|
|
Engine::destroySingleton();
|
|
}
|
|
|
|
UTEST( UIHTML, BlockList ) {
|
|
Engine::instance()->createWindow( WindowSettings( 1024, 768, "Block List Test",
|
|
WindowStyle::Default, WindowBackend::Default,
|
|
32, {}, 1, false, true ),
|
|
ContextSettings( false, 0, 0, GLv_default, true, false ) );
|
|
|
|
UISceneNode* sceneNode = init_test_inline_block();
|
|
|
|
const std::string html = R"html(
|
|
<ul id="block-list">
|
|
<li style="height: 20px">Item 1</li>
|
|
<li style="height: 20px">Item 2</li>
|
|
</ul>
|
|
)html";
|
|
|
|
sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( html ) );
|
|
sceneNode->update( Seconds( 1 ) );
|
|
|
|
auto ul = sceneNode->getRoot()->findByTag( "ul" );
|
|
ASSERT_TRUE( ul != nullptr );
|
|
EXPECT_GT( ul->getPixelsSize().getWidth(), 0 );
|
|
|
|
auto lis = ul->findAllByTag( "li" );
|
|
EXPECT_EQ( lis.size(), (size_t)2 );
|
|
|
|
for ( auto li : lis ) {
|
|
EXPECT_EQ( li->asType<UIHTMLWidget>()->getDisplay(), CSSDisplay::ListItem );
|
|
EXPECT_GT( li->getChildCount(), (size_t)0 );
|
|
EXPECT_TRUE( li->asType<UIRichText>()->getRichTextPtr()->getFontStyleConfig().Font !=
|
|
nullptr );
|
|
EXPECT_GT( li->asType<UIRichText>()->getRichTextPtr()->getFontStyleConfig().CharacterSize,
|
|
0 );
|
|
EXPECT_GT( li->asType<UIRichText>()->getRichTextPtr()->getSize().getWidth(), 0 );
|
|
EXPECT_GT( li->asType<UIRichText>()->getRichTextPtr()->getSize().getHeight(), 0 );
|
|
EXPECT_GT( li->getPixelsSize().getWidth(), 0 );
|
|
EXPECT_GT( li->getPixelsSize().getHeight(), 0 );
|
|
}
|
|
|
|
// They should be one above the other (block)
|
|
EXPECT_LT( lis[0]->getPixelsPosition().y, lis[1]->getPixelsPosition().y );
|
|
EXPECT_EQ( lis[0]->getPixelsPosition().x, lis[1]->getPixelsPosition().x );
|
|
|
|
Engine::destroySingleton();
|
|
}
|
|
|
|
UTEST( UIHTML, InlineList ) {
|
|
Engine::instance()->createWindow( WindowSettings( 1024, 768, "Inline List Test",
|
|
WindowStyle::Default, WindowBackend::Default,
|
|
32, {}, 1, false, true ),
|
|
ContextSettings( false, 0, 0, GLv_default, true, false ) );
|
|
|
|
UISceneNode* sceneNode = init_test_inline_block();
|
|
|
|
const std::string html = R"html(
|
|
<ul style="display: block">
|
|
<li style="display: inline">Item 1</li>
|
|
<li style="display: inline">Item 2</li>
|
|
</ul>
|
|
)html";
|
|
|
|
sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( html ) );
|
|
sceneNode->update( Seconds( 1 ) );
|
|
|
|
auto ul = sceneNode->getRoot()->findByTag( "ul" );
|
|
ASSERT_TRUE( ul != nullptr );
|
|
EXPECT_GT( ul->getPixelsSize().getWidth(), 0 );
|
|
|
|
auto lis = ul->findAllByTag( "li" );
|
|
EXPECT_EQ( lis.size(), (size_t)2 );
|
|
|
|
for ( auto li : lis ) {
|
|
EXPECT_EQ( li->asType<UIHTMLWidget>()->getDisplay(), CSSDisplay::Inline );
|
|
EXPECT_EQ( li->getLayoutWidthPolicy(), SizePolicy::WrapContent );
|
|
EXPECT_GT( li->getPixelsSize().getWidth(), 0 );
|
|
EXPECT_LT( li->getPixelsSize().getWidth(), ul->getPixelsSize().getWidth() );
|
|
}
|
|
|
|
// They should be on the same line (inline)
|
|
EXPECT_EQ( lis[0]->getPixelsPosition().y, lis[1]->getPixelsPosition().y );
|
|
EXPECT_LT( lis[0]->getPixelsPosition().x, lis[1]->getPixelsPosition().x );
|
|
|
|
Engine::destroySingleton();
|
|
}
|
|
|
|
UTEST( UIHTML, InlineBlockExplicitWidth ) {
|
|
Engine::instance()->createWindow( WindowSettings( 1024, 768, "Inline Block Explicit Width Test",
|
|
WindowStyle::Default, WindowBackend::Default,
|
|
32, {}, 1, false, true ),
|
|
ContextSettings( false, 0, 0, GLv_default, true, false ) );
|
|
|
|
UISceneNode* sceneNode = init_test_inline_block();
|
|
|
|
const std::string html = R"html(
|
|
<div style="width: 200px">
|
|
<div id="d1" style="display: inline-block; width: 150px; height: 50px"></div>
|
|
<div id="d2" style="display: inline-block; width: 150px; height: 50px"></div>
|
|
</div>
|
|
)html";
|
|
|
|
sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( html ) );
|
|
sceneNode->update( Seconds( 1 ) );
|
|
|
|
auto d1 = sceneNode->getRoot()->find( "d1" )->asType<UIWidget>();
|
|
auto d2 = sceneNode->getRoot()->find( "d2" )->asType<UIWidget>();
|
|
ASSERT_TRUE( d1 != nullptr && d2 != nullptr );
|
|
|
|
// They should NOT be on the same line because 150 + 150 > 200
|
|
EXPECT_LT( d1->getPixelsPosition().y, d2->getPixelsPosition().y );
|
|
EXPECT_EQ( d1->getPixelsPosition().x, d2->getPixelsPosition().x );
|
|
|
|
Engine::destroySingleton();
|
|
}
|
|
|
|
UTEST( UIHTML, FixedBlockWidthUsesContentBoxWithPadding ) {
|
|
Engine::instance()->createWindow( WindowSettings( 1024, 653, "Fixed Block Content Box Test",
|
|
WindowStyle::Default, WindowBackend::Default,
|
|
32, {}, 1, false, true ),
|
|
ContextSettings( false, 0, 0, GLv_default, true, false ) );
|
|
|
|
UISceneNode* sceneNode = init_test_inline_block();
|
|
|
|
const std::string html = R"html(
|
|
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<style>
|
|
html, body { margin: 0; padding: 0; }
|
|
#wrapper { width: 300px; padding: 0 20px; }
|
|
#borderWrapper { box-sizing: border-box; width: 300px; padding: 0 20px; }
|
|
#child, #borderChild { display: block; width: 100%; height: 10px; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div id="wrapper">
|
|
<div id="child"></div>
|
|
</div>
|
|
<div id="borderWrapper">
|
|
<div id="borderChild"></div>
|
|
</div>
|
|
</body>
|
|
</html>
|
|
)html";
|
|
|
|
sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( html ) );
|
|
sceneNode->update( Seconds( 1 ) );
|
|
sceneNode->updateDirtyLayouts();
|
|
|
|
auto* wrapper = sceneNode->getRoot()->find( "wrapper" )->asType<UIWidget>();
|
|
auto* child = sceneNode->getRoot()->find( "child" )->asType<UIWidget>();
|
|
auto* borderWrapper = sceneNode->getRoot()->find( "borderWrapper" )->asType<UIWidget>();
|
|
auto* borderChild = sceneNode->getRoot()->find( "borderChild" )->asType<UIWidget>();
|
|
|
|
ASSERT_TRUE( wrapper != nullptr );
|
|
ASSERT_TRUE( child != nullptr );
|
|
ASSERT_TRUE( borderWrapper != nullptr );
|
|
ASSERT_TRUE( borderChild != nullptr );
|
|
|
|
EXPECT_NEAR( wrapper->getPixelsSize().getWidth(), 340.f, 1.f );
|
|
EXPECT_NEAR( wrapper->getPixelsSize().getWidth() - wrapper->getPixelsContentOffset().Left -
|
|
wrapper->getPixelsContentOffset().Right,
|
|
300.f, 1.f );
|
|
EXPECT_NEAR( child->getPixelsPosition().x, 20.f, 1.f );
|
|
EXPECT_NEAR( child->getPixelsSize().getWidth(), 300.f, 1.f );
|
|
|
|
EXPECT_NEAR( borderWrapper->getPixelsSize().getWidth(), 300.f, 1.f );
|
|
EXPECT_NEAR( borderWrapper->getPixelsSize().getWidth() -
|
|
borderWrapper->getPixelsContentOffset().Left -
|
|
borderWrapper->getPixelsContentOffset().Right,
|
|
260.f, 1.f );
|
|
EXPECT_NEAR( borderChild->getPixelsPosition().x, 20.f, 1.f );
|
|
EXPECT_NEAR( borderChild->getPixelsSize().getWidth(), 260.f, 1.f );
|
|
|
|
Engine::destroySingleton();
|
|
}
|
|
|
|
UTEST( UIHTML, BoxSizingAppliesToFlexBasis ) {
|
|
Engine::instance()->createWindow( WindowSettings( 1024, 653, "Flex Box Sizing Test",
|
|
WindowStyle::Default, WindowBackend::Default,
|
|
32, {}, 1, false, true ),
|
|
ContextSettings( false, 0, 0, GLv_default, true, false ) );
|
|
|
|
UISceneNode* sceneNode = init_test_inline_block();
|
|
|
|
const std::string html = R"html(
|
|
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<style>
|
|
html, body { margin: 0; padding: 0; }
|
|
#flex { display: flex; width: 800px; }
|
|
.item { flex-grow: 0; flex-shrink: 0; flex-basis: 300px; padding: 0 20px; height: 10px; }
|
|
#borderItem { box-sizing: border-box; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div id="flex">
|
|
<div id="contentItem" class="item"></div>
|
|
<div id="borderItem" class="item"></div>
|
|
</div>
|
|
</body>
|
|
</html>
|
|
)html";
|
|
|
|
sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( html ) );
|
|
sceneNode->update( Seconds( 1 ) );
|
|
sceneNode->updateDirtyLayouts();
|
|
|
|
auto* contentItem = sceneNode->getRoot()->find( "contentItem" )->asType<UIWidget>();
|
|
auto* borderItem = sceneNode->getRoot()->find( "borderItem" )->asType<UIWidget>();
|
|
|
|
ASSERT_TRUE( contentItem != nullptr );
|
|
ASSERT_TRUE( borderItem != nullptr );
|
|
|
|
EXPECT_NEAR( contentItem->getPixelsSize().getWidth(), 340.f, 1.f );
|
|
EXPECT_NEAR( borderItem->getPixelsPosition().x, 340.f, 1.f );
|
|
EXPECT_NEAR( borderItem->getPixelsSize().getWidth(), 300.f, 1.f );
|
|
|
|
Engine::destroySingleton();
|
|
}
|
|
|
|
UTEST( UIHTML, BoxSizingAppliesToGridAndTableContainers ) {
|
|
Engine::instance()->createWindow( WindowSettings( 1024, 653, "Grid Table Box Sizing Test",
|
|
WindowStyle::Default, WindowBackend::Default,
|
|
32, {}, 1, false, true ),
|
|
ContextSettings( false, 0, 0, GLv_default, true, false ) );
|
|
|
|
UISceneNode* sceneNode = init_test_inline_block();
|
|
|
|
const std::string html = R"html(
|
|
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<style>
|
|
html, body { margin: 0; padding: 0; }
|
|
.grid { display: grid; grid-template-columns: 1fr; width: 300px; padding: 0 20px; }
|
|
.table { width: 300px; padding: 0 20px; border-spacing: 0; }
|
|
.border { box-sizing: border-box; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div id="gridContent" class="grid"><div></div></div>
|
|
<div id="gridBorder" class="grid border"><div></div></div>
|
|
<table id="tableContent" class="table"><tr><td>x</td></tr></table>
|
|
<table id="tableBorder" class="table border"><tr><td>x</td></tr></table>
|
|
</body>
|
|
</html>
|
|
)html";
|
|
|
|
sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( html ) );
|
|
sceneNode->update( Seconds( 1 ) );
|
|
sceneNode->updateDirtyLayouts();
|
|
|
|
auto* gridContent = sceneNode->getRoot()->find( "gridContent" )->asType<UIWidget>();
|
|
auto* gridBorder = sceneNode->getRoot()->find( "gridBorder" )->asType<UIWidget>();
|
|
auto* tableContent = sceneNode->getRoot()->find( "tableContent" )->asType<UIWidget>();
|
|
auto* tableBorder = sceneNode->getRoot()->find( "tableBorder" )->asType<UIWidget>();
|
|
|
|
ASSERT_TRUE( gridContent != nullptr );
|
|
ASSERT_TRUE( gridBorder != nullptr );
|
|
ASSERT_TRUE( tableContent != nullptr );
|
|
ASSERT_TRUE( tableBorder != nullptr );
|
|
|
|
EXPECT_NEAR( gridContent->getPixelsSize().getWidth(), 340.f, 1.f );
|
|
EXPECT_NEAR( gridBorder->getPixelsSize().getWidth(), 300.f, 1.f );
|
|
EXPECT_NEAR( tableContent->getPixelsSize().getWidth(), 300.f, 1.f );
|
|
EXPECT_NEAR( tableContent->getPixelsSize().getWidth() -
|
|
tableContent->getPixelsContentOffset().Left -
|
|
tableContent->getPixelsContentOffset().Right,
|
|
260.f, 1.f );
|
|
EXPECT_NEAR( tableBorder->getPixelsSize().getWidth(), 300.f, 1.f );
|
|
EXPECT_NEAR( tableBorder->getPixelsSize().getWidth() -
|
|
tableBorder->getPixelsContentOffset().Left -
|
|
tableBorder->getPixelsContentOffset().Right,
|
|
260.f, 1.f );
|
|
|
|
Engine::destroySingleton();
|
|
}
|
|
|
|
UTEST( UIHTML, InlineBlockMixedContent ) {
|
|
Engine::instance()->createWindow( WindowSettings( 1024, 653, "Inline Block Mixed Content Test",
|
|
WindowStyle::Default, WindowBackend::Default,
|
|
32, {}, 1, false, true ),
|
|
ContextSettings( false, 0, 0, GLv_default, true, false ) );
|
|
|
|
UISceneNode* sceneNode = init_test_inline_block();
|
|
|
|
const std::string html = R"html(
|
|
<div>
|
|
Some text
|
|
<div id="ib" style="display: inline-block; width: 50px; height: 50px; background-color: red"></div>
|
|
more text
|
|
</div>
|
|
)html";
|
|
|
|
sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( html ) );
|
|
sceneNode->update( Seconds( 1 ) );
|
|
|
|
auto ib = sceneNode->getRoot()->find( "ib" )->asType<UIWidget>();
|
|
ASSERT_TRUE( ib != nullptr );
|
|
|
|
// The inline-block should have a non-zero position and be somewhat centered vertically if it
|
|
// follows text flow
|
|
EXPECT_GT( ib->getPixelsPosition().x, 0 );
|
|
EXPECT_GT( ib->getPixelsSize().getWidth(), 0 );
|
|
|
|
Engine::destroySingleton();
|
|
}
|
|
|
|
UTEST( UIHTML, InlineBlockWrapIssue ) {
|
|
Engine::instance()->createWindow( WindowSettings( 1024, 653, "Inline Block Wrap Issue Test",
|
|
WindowStyle::Default, WindowBackend::Default,
|
|
32, {}, 1, false, true ),
|
|
ContextSettings( false, 0, 0, GLv_default, true, false ) );
|
|
|
|
UISceneNode* sceneNode = init_test_inline_block();
|
|
|
|
std::string html;
|
|
FileSystem::fileGet( "assets/html/inline_block_wrap.html", html );
|
|
|
|
sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( html ) );
|
|
sceneNode->update( Seconds( 1 ) );
|
|
|
|
auto h2 = sceneNode->getRoot()->find( "h2-wrap" );
|
|
ASSERT_TRUE( h2 != nullptr );
|
|
|
|
auto rt = h2->asType<UIRichText>()->getRichTextPtr();
|
|
|
|
EXPECT_EQ( (size_t)1, rt->getLines().size() );
|
|
|
|
Engine::destroySingleton();
|
|
}
|
|
|
|
UTEST( UIHTML, DeferredInlineBlockListDoesNotWrapItems ) {
|
|
Engine::instance()->createWindow( WindowSettings( 1024, 653, "Deferred Inline Block List Test",
|
|
WindowStyle::Default, WindowBackend::Default,
|
|
32, {}, 1, false, true ),
|
|
ContextSettings( false, 0, 0, GLv_default, true ) );
|
|
FileSystem::changeWorkingDirectory( Sys::getProcessPath() );
|
|
|
|
auto* win = Engine::instance()->getCurrentWindow();
|
|
UISceneNode* sceneNode = init_test_inline_block();
|
|
|
|
UIWebView* webView = UIWebView::New();
|
|
webView->setParent( sceneNode->getRoot() );
|
|
webView->setPixelsSize( 1280, 600 );
|
|
webView->setLayoutSizePolicy( SizePolicy::Fixed, SizePolicy::Fixed );
|
|
webView->loadURI( URI( "./assets/html/inline_block_wrap_defer.html" ) );
|
|
|
|
UISceneNode* documentScene = webView->getDocumentSceneNode();
|
|
ASSERT_TRUE( documentScene != nullptr );
|
|
|
|
UIWidget* ul = nullptr;
|
|
|
|
for ( int i = 0; i < 100; i++ ) {
|
|
win->getInput()->update();
|
|
SceneManager::instance()->update( Milliseconds( 16 ) );
|
|
ul = documentScene->getRoot()->findByTag( "ul" );
|
|
if ( ul ) {
|
|
auto lis = ul->findAllByTag( "li" );
|
|
if ( !lis.empty() &&
|
|
lis.front()->asType<UIHTMLWidget>()->getDisplay() == CSSDisplay::InlineBlock )
|
|
break;
|
|
}
|
|
Sys::sleep( Milliseconds( 1 ) );
|
|
}
|
|
|
|
ASSERT_TRUE( ul != nullptr );
|
|
auto lis = ul->findAllByTag( "li" );
|
|
ASSERT_EQ( lis.size(), (size_t)4 );
|
|
|
|
const Color expectedLinkColor( "#B58900" );
|
|
const Float rowY = lis.front()->getPixelsPosition().y;
|
|
Float previousRight = lis.front()->getPixelsPosition().x;
|
|
|
|
for ( auto li : lis ) {
|
|
auto* htmlLi = li->asType<UIHTMLWidget>();
|
|
auto* richLi = li->asType<UIRichText>();
|
|
ASSERT_TRUE( htmlLi != nullptr );
|
|
ASSERT_TRUE( richLi != nullptr );
|
|
auto* anchor = li->findByTag( "a" );
|
|
ASSERT_TRUE( anchor != nullptr );
|
|
auto* htmlAnchor = anchor->asType<UIHTMLWidget>();
|
|
auto* richAnchor = anchor->asType<UIRichText>();
|
|
auto* textAnchor = anchor->asType<UITextSpan>();
|
|
ASSERT_TRUE( htmlAnchor != nullptr );
|
|
ASSERT_TRUE( richAnchor != nullptr );
|
|
ASSERT_TRUE( textAnchor != nullptr );
|
|
|
|
EXPECT_EQ( htmlLi->getDisplay(), CSSDisplay::InlineBlock );
|
|
EXPECT_EQ( htmlAnchor->getDisplay(), CSSDisplay::InlineBlock );
|
|
EXPECT_EQ( textAnchor->getFontColor().getValue(), expectedLinkColor.getValue() );
|
|
EXPECT_EQ( richLi->getRichTextPtr()->getLines().size(), (size_t)1 );
|
|
EXPECT_EQ( richAnchor->getRichTextPtr()->getLines().size(), (size_t)1 );
|
|
const auto& anchorLine = richAnchor->getRichTextPtr()->getLines().front();
|
|
const Rectf liContentOffset = li->asType<UIWidget>()->getPixelsContentOffset();
|
|
const Rectf anchorContentOffset = anchor->asType<UIWidget>()->getPixelsContentOffset();
|
|
EXPECT_NEAR( li->getPixelsSize().getHeight(),
|
|
anchorLine.height + liContentOffset.Top + liContentOffset.Bottom, 1.f );
|
|
EXPECT_NEAR( anchor->getPixelsSize().getHeight(),
|
|
anchorLine.height + anchorContentOffset.Top + anchorContentOffset.Bottom,
|
|
1.f );
|
|
EXPECT_EQ( li->getPixelsPosition().y, rowY );
|
|
EXPECT_GE( li->getPixelsPosition().x, previousRight );
|
|
EXPECT_GT( li->getPixelsSize().getWidth(), 0 );
|
|
EXPECT_GE( li->getPixelsSize().getWidth() + 1.f, li->getMaxIntrinsicWidth() );
|
|
EXPECT_GE( anchor->getPixelsSize().getWidth() + 1.f, anchor->getMaxIntrinsicWidth() );
|
|
EXPECT_TRUE( textAnchor->getHitBoxes().empty() );
|
|
|
|
Vector2f lowerRightHitPoint( anchor->getPixelsSize().getWidth() - 1.f,
|
|
anchor->getPixelsSize().getHeight() - 1.f );
|
|
anchor->nodeToWorld( lowerRightHitPoint );
|
|
EXPECT_EQ( documentScene->overFind( lowerRightHitPoint ), anchor );
|
|
|
|
previousRight = li->getPixelsPosition().x + li->getPixelsSize().getWidth();
|
|
}
|
|
|
|
Engine::destroySingleton();
|
|
}
|
|
|
|
UTEST( UIHTML, InlineBlockBrowserTest ) {
|
|
Engine::instance()->createWindow( WindowSettings( 1024, 653, "Inline Block Browser Test",
|
|
WindowStyle::Default, WindowBackend::Default,
|
|
32, {}, 1, false, true ),
|
|
ContextSettings( false, 0, 0, GLv_default, true, false ) );
|
|
|
|
UISceneNode* sceneNode = init_test_inline_block();
|
|
std::string html;
|
|
FileSystem::fileGet( "assets/html/is_inline_block.html", html );
|
|
|
|
sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( html ) );
|
|
sceneNode->update( Seconds( 1 ) );
|
|
|
|
auto ib = sceneNode->getRoot()->find( "ib" )->asType<UIWidget>();
|
|
auto t1 = sceneNode->getRoot()->find( "t1" )->asType<UIWidget>();
|
|
auto t2 = sceneNode->getRoot()->find( "t2" )->asType<UIWidget>();
|
|
|
|
ASSERT_TRUE( ib != nullptr && t1 != nullptr && t2 != nullptr );
|
|
|
|
// If it drops to the next line:
|
|
EXPECT_GT( ib->getPixelsPosition().y, t1->getPixelsPosition().y );
|
|
// And t2 should be AFTER ib (either horizontally or vertically)
|
|
EXPECT_GE( t2->getPixelsPosition().y,
|
|
ib->getPixelsPosition().y + ib->getPixelsSize().getHeight() );
|
|
if ( t2->getPixelsPosition().y == ib->getPixelsPosition().y ) {
|
|
EXPECT_GE( t2->getPixelsPosition().x,
|
|
ib->getPixelsPosition().x + ib->getPixelsSize().getWidth() );
|
|
}
|
|
EXPECT_EQ( ib->getPixelsPosition().x, t2->getPixelsPosition().x );
|
|
|
|
Engine::destroySingleton();
|
|
}
|
|
|
|
UTEST( UIHTML, HeightExpansion ) {
|
|
Engine::instance()->createWindow( WindowSettings( 1024, 653, "Height Expansion Test",
|
|
WindowStyle::Default, WindowBackend::Default,
|
|
32, {}, 1, false, true ),
|
|
ContextSettings( false, 0, 0, GLv_default, true, false ) );
|
|
|
|
UISceneNode* sceneNode = init_test_inline_block();
|
|
sceneNode->setURI( "file://" + Sys::getProcessPath() + "assets/html/ensoft/" );
|
|
|
|
std::string html;
|
|
FileSystem::fileGet( "assets/html/ensoft/ensoft.html", html );
|
|
|
|
sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( html ) );
|
|
sceneNode->update( Seconds( 1 ) );
|
|
|
|
// Wait a bit and update again to make sure layouts are computed
|
|
sceneNode->updateDirtyLayouts();
|
|
|
|
auto htmlNode = sceneNode->getRoot()->findByType( UI_TYPE_HTML_HTML );
|
|
auto bodyNode = sceneNode->getRoot()->findByType( UI_TYPE_HTML_BODY );
|
|
|
|
ASSERT_TRUE( htmlNode != nullptr );
|
|
ASSERT_TRUE( bodyNode != nullptr );
|
|
|
|
auto htmlWidget = htmlNode->asType<UIWidget>();
|
|
auto bodyWidget = bodyNode->asType<UIWidget>();
|
|
|
|
EXPECT_GT( htmlWidget->getSize().getHeight(), 0 );
|
|
EXPECT_GT( bodyWidget->getSize().getHeight(), 0 );
|
|
|
|
EXPECT_GE( htmlWidget->getSize().getHeight(), bodyWidget->getSize().getHeight() );
|
|
|
|
auto barNode = sceneNode->getRoot()->find( "bar" );
|
|
ASSERT_TRUE( barNode != nullptr );
|
|
|
|
auto barWidget = barNode->asType<UIWidget>();
|
|
auto barHTML = barNode->asType<UIHTMLWidget>();
|
|
|
|
EXPECT_EQ( barHTML->getCSSPosition(), CSSPosition::Fixed );
|
|
|
|
EXPECT_NEAR( barWidget->getPixelsSize().getWidth(), 250.f, 1.f );
|
|
|
|
auto rootWidget = sceneNode->getRoot();
|
|
EXPECT_GT( rootWidget->getPixelsSize().getHeight(), 0 );
|
|
EXPECT_NEAR( barWidget->getPixelsSize().getHeight(), rootWidget->getPixelsSize().getHeight(),
|
|
1.f );
|
|
|
|
Engine::destroySingleton();
|
|
}
|
|
|
|
UTEST( UIHTML, HeightExpansion_FixedDoesNotExpand ) {
|
|
Engine::instance()->createWindow( WindowSettings( 1024, 653, "Height Expansion Test",
|
|
WindowStyle::Default, WindowBackend::Default,
|
|
32, {}, 1, false, true ),
|
|
ContextSettings( false, 0, 0, GLv_default, true, false ) );
|
|
|
|
UISceneNode* sceneNode = init_test_inline_block();
|
|
|
|
const std::string html = R"html(
|
|
<!DOCTYPE html>
|
|
<html>
|
|
<body style="margin: 0; padding: 0;">
|
|
<div style="height: 100px;"></div>
|
|
<div style="position: fixed; top: 500px; height: 50px;"></div>
|
|
</body>
|
|
</html>
|
|
)html";
|
|
|
|
sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( html ) );
|
|
sceneNode->update( Seconds( 1 ) );
|
|
sceneNode->updateDirtyLayouts();
|
|
|
|
auto bodyNode = sceneNode->getRoot()->findByType( UI_TYPE_HTML_BODY );
|
|
ASSERT_TRUE( bodyNode != nullptr );
|
|
|
|
auto bodyWidget = bodyNode->asType<UIWidget>();
|
|
|
|
// The height should be 100px, not 550px because the fixed div should be ignored.
|
|
EXPECT_NEAR( bodyWidget->getPixelsSize().getHeight(), 100.f, 1.f );
|
|
|
|
Engine::destroySingleton();
|
|
}
|
|
|
|
UTEST( UIHTML, BodyHeightMiscalculationFixture ) {
|
|
Engine::instance()->createWindow( WindowSettings( 1024, 653, "Body Height Miscalculation Test",
|
|
WindowStyle::Default, WindowBackend::Default,
|
|
32, {}, 1, false, true ),
|
|
ContextSettings( false, 0, 0, GLv_default, true, false ) );
|
|
|
|
UISceneNode* sceneNode = init_test_inline_block();
|
|
sceneNode->setURI( "file://" + Sys::getProcessPath() + "assets/html/" );
|
|
|
|
std::string html;
|
|
FileSystem::fileGet( "assets/html/body_height_miscalculation.html", html );
|
|
|
|
sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( html ) );
|
|
sceneNode->update( Seconds( 1 ) );
|
|
sceneNode->updateDirtyLayouts();
|
|
|
|
auto bodyNode = sceneNode->getRoot()->findByType( UI_TYPE_HTML_BODY );
|
|
ASSERT_TRUE( bodyNode != nullptr );
|
|
|
|
auto bodyWidget = bodyNode->asType<UIWidget>();
|
|
|
|
EXPECT_GT( bodyWidget->getPixelsSize().getHeight(), 3000.f );
|
|
EXPECT_LT( bodyWidget->getPixelsSize().getHeight(), 6000.f );
|
|
|
|
// Verify the nav layout: .wrap has justify-content: space-between,
|
|
// so .brand should be at the left and .links at the right.
|
|
auto nav = sceneNode->getRoot()->findByClass( "site-nav" );
|
|
ASSERT_TRUE( nav != nullptr );
|
|
auto navWidget = nav->asType<UIWidget>();
|
|
|
|
auto wrap = navWidget->findByClass( "wrap" );
|
|
ASSERT_TRUE( wrap != nullptr );
|
|
auto wrapWidget = wrap->asType<UIWidget>();
|
|
|
|
auto brand = wrapWidget->findByClass( "brand" );
|
|
ASSERT_TRUE( brand != nullptr );
|
|
auto brandWidget = brand->asType<UIWidget>();
|
|
|
|
auto links = wrapWidget->findByClass( "links" );
|
|
ASSERT_TRUE( links != nullptr );
|
|
auto linksWidget = links->asType<UIWidget>();
|
|
|
|
// With justify-content: space-between in a row-direction flex:
|
|
// .brand should be at the left side of .wrap (near padding edge).
|
|
EXPECT_LT( brandWidget->getPixelsPosition().x, wrapWidget->getPixelsSize().getWidth() * 0.25f );
|
|
|
|
// .links should be at the right side of .wrap, NOT next to .brand.
|
|
Float wrapW = wrapWidget->getPixelsSize().getWidth();
|
|
Float linksW = linksWidget->getPixelsSize().getWidth();
|
|
EXPECT_GT( linksWidget->getPixelsPosition().x, wrapW * 0.5f );
|
|
|
|
// .links should NOT occupy all available width — it must be content-sized.
|
|
EXPECT_GT( linksW, 0.f );
|
|
EXPECT_LT( linksW, wrapW * 0.5f );
|
|
|
|
Engine::destroySingleton();
|
|
}
|
|
|
|
UTEST( UIHTML, ContactFormLayout ) {
|
|
Engine::instance()->createWindow( WindowSettings( 1024, 653, "Contact Form Layout Test",
|
|
WindowStyle::Default, WindowBackend::Default,
|
|
32, {}, 1, false, true ),
|
|
ContextSettings( false, 0, 0, GLv_default, true, false ) );
|
|
|
|
UISceneNode* sceneNode = init_test_inline_block();
|
|
sceneNode->setURI( "file://" + Sys::getProcessPath() + "assets/html/ensoft/" );
|
|
|
|
std::string html;
|
|
FileSystem::fileGet( "assets/html/ensoft/contact.html", html );
|
|
sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( html ) );
|
|
|
|
sceneNode->update( Seconds( 1 ) );
|
|
sceneNode->updateDirtyLayouts();
|
|
|
|
auto form = sceneNode->getRoot()->find( "form-contact" );
|
|
ASSERT_TRUE( form != nullptr );
|
|
auto formWidget = form->asType<UIWidget>();
|
|
EXPECT_GT( formWidget->getPixelsSize().getHeight(), 0 );
|
|
|
|
auto ul = formWidget->findByTag( "ul" );
|
|
ASSERT_TRUE( ul != nullptr );
|
|
auto ulWidget = ul->asType<UIWidget>();
|
|
Float ulHeight = ulWidget->getPixelsSize().getHeight();
|
|
EXPECT_GT( ulHeight, 0 );
|
|
|
|
auto lis = ulWidget->findAllByTag( "li" );
|
|
EXPECT_EQ( lis.size(), (size_t)7 );
|
|
|
|
Float totalLiHeight = 0;
|
|
int visibleLiCount = 0;
|
|
for ( auto li : lis ) {
|
|
Float liH = li->getPixelsSize().getHeight();
|
|
if ( liH > 0 )
|
|
visibleLiCount++;
|
|
totalLiHeight += liH;
|
|
}
|
|
EXPECT_EQ( visibleLiCount, 6 );
|
|
EXPECT_GT( totalLiHeight, 0 );
|
|
EXPECT_NEAR( ulHeight, totalLiHeight, 1.f );
|
|
|
|
auto contactBox = sceneNode->getRoot()->find( "contact-box" );
|
|
ASSERT_TRUE( contactBox != nullptr );
|
|
auto cbWidget = contactBox->asType<UIWidget>();
|
|
EXPECT_GT( cbWidget->getPixelsSize().getHeight(), 10 );
|
|
|
|
auto content = sceneNode->getRoot()->find( "content" );
|
|
ASSERT_TRUE( content != nullptr );
|
|
auto contentWidget = content->asType<UIWidget>();
|
|
EXPECT_GT( contentWidget->getPixelsSize().getHeight(), 60 );
|
|
|
|
auto bodyNode = sceneNode->getRoot()->findByType( UI_TYPE_HTML_BODY );
|
|
ASSERT_TRUE( bodyNode != nullptr );
|
|
auto bodyWidget = bodyNode->asType<UIWidget>();
|
|
EXPECT_GT( bodyWidget->getPixelsSize().getHeight(), 0 );
|
|
|
|
Engine::destroySingleton();
|
|
}
|
|
|
|
UTEST( UIBackground, shorthandImageVariableAfterSize ) {
|
|
auto* specification = StyleSheetSpecification::instance();
|
|
const auto* shorthand = specification->getShorthand( "background" );
|
|
ASSERT_TRUE( shorthand != nullptr );
|
|
|
|
auto properties = shorthand->parse(
|
|
"no-repeat center/100% var(--sf-img-1), linear-gradient(transparent, transparent)" );
|
|
auto valueOf = [&properties]( const std::string& name ) -> std::string {
|
|
for ( const auto& property : properties ) {
|
|
if ( property.getName() == name )
|
|
return property.getValue();
|
|
}
|
|
return {};
|
|
};
|
|
|
|
EXPECT_STDSTREQ( valueOf( "background-image" ),
|
|
"var(--sf-img-1),linear-gradient(transparent, transparent)" );
|
|
EXPECT_STDSTREQ( valueOf( "background-size" ), "100%,auto" );
|
|
EXPECT_STDSTREQ( valueOf( "background-position-x" ), "center,0%" );
|
|
EXPECT_STDSTREQ( valueOf( "background-position-y" ), "center,0%" );
|
|
}
|
|
|
|
UTEST( UIBackground, imageAtlasPositioning ) {
|
|
auto win = Engine::instance()->createWindow(
|
|
WindowSettings( 1024, 653, "Background Atlas Test", VisualTestWindowStyle,
|
|
WindowBackend::Default, 32, {}, 1, false, true ),
|
|
ContextSettings( false, 0, 0, GLv_default, true, false ) );
|
|
FileSystem::changeWorkingDirectory( Sys::getProcessPath() );
|
|
|
|
UISceneNode* sceneNode = init_test_inline_block();
|
|
|
|
sceneNode->setURI( "file://" + Sys::getProcessPath() + "assets/html/" );
|
|
|
|
std::string html;
|
|
FileSystem::fileGet( "assets/html/background_atlas.html", html );
|
|
sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( html ) );
|
|
win->setClearColor( Color::White );
|
|
|
|
// Verify the atlas image was actually loaded — scan all nodes for a loaded drawable
|
|
bool foundLoadedImage = false;
|
|
sceneNode->getRoot()->forEachNode( [&foundLoadedImage]( Node* node ) {
|
|
if ( foundLoadedImage || !node->isWidget() )
|
|
return;
|
|
auto* bg = node->asType<UIWidget>()->getBackground();
|
|
if ( !bg )
|
|
return;
|
|
auto* layer = bg->getLayer( 0 );
|
|
if ( layer && layer->getDrawable() ) {
|
|
Sizef sz = layer->getDrawable()->getPixelsSize();
|
|
if ( sz.getWidth() == 1024.f && sz.getHeight() == 512.f )
|
|
foundLoadedImage = true;
|
|
}
|
|
} );
|
|
ASSERT_TRUE( foundLoadedImage );
|
|
|
|
win->getInput()->update();
|
|
SceneManager::instance()->update();
|
|
|
|
win->clear();
|
|
SceneManager::instance()->draw();
|
|
win->display();
|
|
|
|
compareImages( utest_state, utest_result, win, "eepp-ui-background-atlas", "html", 4 );
|
|
|
|
Engine::destroySingleton();
|
|
}
|
|
|
|
UTEST( UIBackground, inlineSpanColorRendersBehindBackgroundImage ) {
|
|
auto win = Engine::instance()->createWindow(
|
|
WindowSettings( 320, 160, "Inline Background Color Test", WindowStyle::Default,
|
|
WindowBackend::Default, 32, {}, 1, false, true ),
|
|
ContextSettings( false, 0, 0, GLv_default, true, false ) );
|
|
FileSystem::changeWorkingDirectory( Sys::getProcessPath() );
|
|
|
|
UISceneNode* sceneNode = init_test_inline_block();
|
|
sceneNode->setURI( "file://" + Sys::getProcessPath() + "assets/html/ensoft/" );
|
|
|
|
std::string html;
|
|
FileSystem::fileGet( "assets/html/ensoft/background.html", html );
|
|
sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( html ) );
|
|
win->setClearColor( Color::White );
|
|
|
|
win->getInput()->update();
|
|
SceneManager::instance()->update();
|
|
|
|
win->clear();
|
|
SceneManager::instance()->draw();
|
|
win->display();
|
|
|
|
auto anchors = sceneNode->getRoot()->querySelectorAll( "#rss a" );
|
|
ASSERT_EQ( anchors.size(), (size_t)1 );
|
|
auto* anchor = anchors.front()->asType<UITextSpan>();
|
|
ASSERT_TRUE( anchor != nullptr );
|
|
EXPECT_EQ( anchor->getFontBackgroundColor().getValue(),
|
|
Color::fromString( "#d0d0d0" ).getValue() );
|
|
ASSERT_TRUE( anchor->getBackground() != nullptr );
|
|
ASSERT_TRUE( anchor->getBackground()->hasDrawableLayers() );
|
|
ASSERT_TRUE( anchor->getBackground()->getBackgroundDrawable().hasRadius() );
|
|
|
|
bool foundRoundedLayeredFragment = false;
|
|
sceneNode->getRoot()->forEachNode( [&]( Node* node ) {
|
|
if ( foundRoundedLayeredFragment || !node->isType( UI_TYPE_RICHTEXT ) )
|
|
return;
|
|
for ( const auto& fragment :
|
|
node->asType<UIRichText>()->getRichText().getInlineFragments() ) {
|
|
if ( fragment.type == RichText::InlineFragment::Type::Box &&
|
|
fragment.source.type == RichText::InlineSourceType::Widget &&
|
|
fragment.source.ptr == anchor ) {
|
|
foundRoundedLayeredFragment = true;
|
|
EXPECT_TRUE( fragment.backgroundColorDrawable != nullptr );
|
|
EXPECT_TRUE( fragment.backgroundDrawable != nullptr );
|
|
EXPECT_EQ( fragment.backgroundColor.getValue(),
|
|
Color::fromString( "#d0d0d0" ).getValue() );
|
|
break;
|
|
}
|
|
}
|
|
} );
|
|
EXPECT_TRUE( foundRoundedLayeredFragment );
|
|
|
|
const Rectf rect = anchor->getScreenRect();
|
|
ASSERT_GT( rect.getWidth(), 12.f );
|
|
ASSERT_GT( rect.getHeight(), 4.f );
|
|
|
|
Image image = win->getFrontBufferImage();
|
|
const Uint32 sampleX = static_cast<Uint32>( eefloor( rect.Right - 5.f ) );
|
|
const Uint32 sampleY = static_cast<Uint32>( eefloor( rect.Top + rect.getHeight() * 0.5f ) );
|
|
ASSERT_LT( sampleX, image.getWidth() );
|
|
ASSERT_LT( sampleY, image.getHeight() );
|
|
|
|
const Color pixel = image.getPixel( sampleX, sampleY );
|
|
EXPECT_NEAR( pixel.r, 0xd0, 4 );
|
|
EXPECT_NEAR( pixel.g, 0xd0, 4 );
|
|
EXPECT_NEAR( pixel.b, 0xd0, 4 );
|
|
|
|
Engine::destroySingleton();
|
|
}
|
|
|
|
UTEST( UIBackground, imageAtlasPositioningPixelDensity2 ) {
|
|
auto win = Engine::instance()->createWindow(
|
|
WindowSettings( 1024, 653, "Background Atlas Test PD2", VisualTestWindowStyle,
|
|
WindowBackend::Default, 32, {}, 1, false, true ),
|
|
ContextSettings( false, 0, 0, GLv_default, true, false ) );
|
|
EE::Graphics::PixelDensity::setPixelDensity( 2.0f );
|
|
FileSystem::changeWorkingDirectory( Sys::getProcessPath() );
|
|
|
|
UISceneNode* sceneNode = init_test_inline_block();
|
|
sceneNode->setURI( "file://" + Sys::getProcessPath() + "assets/html/" );
|
|
|
|
std::string html;
|
|
FileSystem::fileGet( "assets/html/background_atlas.html", html );
|
|
sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( html ) );
|
|
win->setClearColor( Color::White );
|
|
|
|
win->getInput()->update();
|
|
SceneManager::instance()->update();
|
|
|
|
win->clear();
|
|
SceneManager::instance()->draw();
|
|
win->display();
|
|
|
|
compareImages( utest_state, utest_result, win, "eepp-ui-background-atlas-pd2", "html", 4 );
|
|
|
|
Engine::destroySingleton();
|
|
EE::Graphics::PixelDensity::setPixelDensity( 1.0f );
|
|
}
|
|
|
|
UTEST( UIBackground, cssFileRelativeSpriteUrlAndNegativePosition ) {
|
|
init_ui_test();
|
|
UISceneNode* sceneNode = SceneManager::instance()->getUISceneNode();
|
|
sceneNode->setURI( "file://" + Sys::getProcessPath() + "assets/html/" );
|
|
|
|
sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( R"html(
|
|
<body style="margin:0">
|
|
<div id="sprite" class="sprite"></div>
|
|
</body>
|
|
)html" ) );
|
|
sceneNode->combineStyleSheet(
|
|
R"css(
|
|
.sprite {
|
|
width: 15px;
|
|
height: 14px;
|
|
background-image: url(sprite-reddit.13AvZYXRW_4.png);
|
|
background-position: -42px -1678px;
|
|
background-repeat: no-repeat;
|
|
}
|
|
)css",
|
|
true, String::hash( "reddit-sprite-test" ),
|
|
URI( "file://" + Sys::getProcessPath() +
|
|
"assets/html/reddit_old_thread_files/reddit-sprite-test.css" ) );
|
|
sceneNode->updateDirtyLayouts();
|
|
|
|
auto* sprite = sceneNode->find<UIWidget>( "sprite" );
|
|
ASSERT_TRUE( sprite != nullptr );
|
|
auto* background = sprite->getBackground();
|
|
ASSERT_TRUE( background != nullptr );
|
|
auto* layer = background->getLayer( 0 );
|
|
ASSERT_TRUE( layer != nullptr );
|
|
ASSERT_TRUE( layer->getDrawable() != nullptr );
|
|
|
|
EXPECT_EQ( layer->getDrawable()->getPixelsSize().getWidth(), 140 );
|
|
EXPECT_EQ( layer->getDrawable()->getPixelsSize().getHeight(), 1751 );
|
|
EXPECT_STDSTREQ( layer->getPositionX(), "-42px" );
|
|
EXPECT_STDSTREQ( layer->getPositionY(), "-1678px" );
|
|
EXPECT_NEAR( layer->getOffset().x, -42.f, 0.1f );
|
|
EXPECT_NEAR( layer->getOffset().y, -1678.f, 0.1f );
|
|
EXPECT_EQ( layer->getRepeatX(), UINodeDrawable::RepeatX::NoRepeat );
|
|
EXPECT_EQ( layer->getRepeatY(), UINodeDrawable::RepeatY::NoRepeat );
|
|
|
|
Engine::destroySingleton();
|
|
}
|
|
|
|
UTEST( UIBackground, RemoteImageReusesCachedTexture ) {
|
|
Engine::instance()->createWindow( WindowSettings( 1024, 768, "remote bg cache reuse",
|
|
WindowStyle::Default, WindowBackend::Default,
|
|
32, {}, 1, false, true ),
|
|
ContextSettings( false, 0, 0, GLv_default, true, false ) );
|
|
UISceneNode* sceneNode = init_test_inline_block();
|
|
const std::string imageURL = "http://127.0.0.1:1/eepp-cached-background.png";
|
|
TexturePtr cached = TextureFactory::instance()->createEmptyTexture(
|
|
8, 8, 4, Color::White, false, Texture::ClampMode::ClampToEdge, false, false, imageURL );
|
|
ASSERT_TRUE( cached != nullptr );
|
|
sceneNode->getResourceScope()->publishLocal( imageURL, cached );
|
|
|
|
sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( R"html(
|
|
<body>
|
|
<div id="first" style="width:10px;height:10px;background-image:url('http://127.0.0.1:1/eepp-cached-background.png')"></div>
|
|
<div id="second" style="width:10px;height:10px;background-image:url('http://127.0.0.1:1/eepp-cached-background.png')"></div>
|
|
</body>
|
|
)html" ) );
|
|
sceneNode->updateDirtyLayouts();
|
|
|
|
auto* first = sceneNode->find<UIWidget>( "first" );
|
|
auto* second = sceneNode->find<UIWidget>( "second" );
|
|
ASSERT_TRUE( first != nullptr );
|
|
ASSERT_TRUE( second != nullptr );
|
|
ASSERT_TRUE( first->getBackground() != nullptr );
|
|
ASSERT_TRUE( second->getBackground() != nullptr );
|
|
ASSERT_TRUE( first->getBackground()->getLayer( 0 ) != nullptr );
|
|
ASSERT_TRUE( second->getBackground()->getLayer( 0 ) != nullptr );
|
|
const DrawablePtr& firstDrawable = first->getBackground()->getLayer( 0 )->getDrawable();
|
|
const DrawablePtr& secondDrawable = second->getBackground()->getLayer( 0 )->getDrawable();
|
|
EXPECT_EQ( cached.get(), getDrawableTexture( firstDrawable ) );
|
|
EXPECT_EQ( cached.get(), getDrawableTexture( secondDrawable ) );
|
|
EXPECT_NE( firstDrawable.get(), secondDrawable.get() );
|
|
|
|
cached.reset();
|
|
Engine::destroySingleton();
|
|
}
|
|
|
|
UTEST( UIBackground, InlineBlockImageSpans ) {
|
|
auto win = Engine::instance()->createWindow(
|
|
WindowSettings( 1024, 653, "inline-block image spans", VisualTestWindowStyle,
|
|
WindowBackend::Default, 32, {}, 1, false, true ),
|
|
ContextSettings( false, 0, 0, GLv_default, true, false ) );
|
|
FileSystem::changeWorkingDirectory( Sys::getProcessPath() );
|
|
|
|
UISceneNode* sceneNode = init_test_inline_block();
|
|
|
|
sceneNode->setURI( "file://" + Sys::getProcessPath() + "assets/html/" );
|
|
|
|
std::string html;
|
|
FileSystem::fileGet( "assets/html/inline_block.html", html );
|
|
sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( html ) );
|
|
win->setClearColor( Color::White );
|
|
|
|
win->getInput()->update();
|
|
SceneManager::instance()->update();
|
|
|
|
win->clear();
|
|
SceneManager::instance()->draw();
|
|
win->display();
|
|
|
|
auto anchors = sceneNode->getRoot()->findAllByTag( "a" );
|
|
auto spans = sceneNode->getRoot()->querySelectorAll( "a > span" );
|
|
|
|
EXPECT_GT( anchors.size(), (size_t)0 );
|
|
EXPECT_GT( spans.size(), (size_t)0 );
|
|
|
|
for ( auto anchor : anchors ) {
|
|
EXPECT_GT( anchor->getPixelsSize().getWidth(), 0 );
|
|
EXPECT_GT( anchor->getPixelsSize().getHeight(), 0 );
|
|
}
|
|
|
|
for ( auto span : spans ) {
|
|
EXPECT_GT( span->getPixelsSize().getWidth(), 0 );
|
|
EXPECT_GT( span->getPixelsSize().getHeight(), 0 );
|
|
}
|
|
|
|
compareImages( utest_state, utest_result, win, "eepp-ui-inline-block-image-spans", "html", 4 );
|
|
|
|
Engine::destroySingleton();
|
|
}
|
|
|
|
UTEST( UIBackground, InlineBlockImageFixedSize ) {
|
|
auto win = Engine::instance()->createWindow(
|
|
WindowSettings( 1024, 653, "inline-block image fixed size", WindowStyle::Default,
|
|
WindowBackend::Default, 32, {}, 1, false, true ),
|
|
ContextSettings( false, 0, 0, GLv_default, true, false ) );
|
|
FileSystem::changeWorkingDirectory( Sys::getProcessPath() );
|
|
|
|
UISceneNode* sceneNode = init_test_inline_block();
|
|
|
|
sceneNode->setURI( "file://" + Sys::getProcessPath() + "assets/html/" );
|
|
|
|
std::string html;
|
|
FileSystem::fileGet( "assets/html/reddit_header_icons.html", html );
|
|
sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( html ) );
|
|
win->setClearColor( Color::White );
|
|
|
|
win->getInput()->update();
|
|
SceneManager::instance()->update();
|
|
|
|
win->clear();
|
|
SceneManager::instance()->draw();
|
|
win->display();
|
|
|
|
auto anchors = sceneNode->getRoot()->findAllByTag( "a" );
|
|
|
|
EXPECT_GT( anchors.size(), (size_t)0 );
|
|
|
|
for ( auto anchor : anchors ) {
|
|
EXPECT_EQ( anchor->getPixelsSize().getWidth(), 15 );
|
|
EXPECT_EQ( anchor->getPixelsSize().getHeight(), 12 );
|
|
}
|
|
|
|
Engine::destroySingleton();
|
|
}
|
|
|
|
UTEST( UIHTML, RedditHeaderPagenameBottomAlign ) {
|
|
auto win = Engine::instance()->createWindow(
|
|
WindowSettings( 1024, 653, "reddit header pagename bottom align", WindowStyle::Default,
|
|
WindowBackend::Default, 32, {}, 1, false, true ),
|
|
ContextSettings( false, 0, 0, GLv_default, true, false ) );
|
|
FileSystem::changeWorkingDirectory( Sys::getProcessPath() );
|
|
|
|
UISceneNode* sceneNode = init_test_inline_block();
|
|
sceneNode->setURI( "file://" + Sys::getProcessPath() + "assets/html/" );
|
|
|
|
std::string html;
|
|
FileSystem::fileGet( "assets/html/reddit_header.html", html );
|
|
sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( html ) );
|
|
win->getInput()->update();
|
|
SceneManager::instance()->update();
|
|
|
|
auto* headerLeft = sceneNode->getRoot()->find( "header-bottom-left" )->asType<UIHTMLWidget>();
|
|
auto* logo = sceneNode->getRoot()->find( "header-img" )->asType<UIHTMLWidget>();
|
|
auto* page = sceneNode->getRoot()->querySelector( ".pagename" )->asType<UIHTMLWidget>();
|
|
auto* jumpToContent = sceneNode->getRoot()->find( "jumpToContent" )->asType<UIHTMLWidget>();
|
|
ASSERT_TRUE( headerLeft != nullptr );
|
|
ASSERT_TRUE( logo != nullptr );
|
|
ASSERT_TRUE( page != nullptr );
|
|
ASSERT_TRUE( jumpToContent != nullptr );
|
|
|
|
EXPECT_EQ( logo->getBaselineAlign().type, CSSBaselineAlignment::Bottom );
|
|
EXPECT_EQ( page->getBaselineAlign().type, CSSBaselineAlignment::Bottom );
|
|
EXPECT_NEAR( headerLeft->getPixelsSize().getHeight(),
|
|
page->getPixelsPosition().y + page->getPixelsSize().getHeight(), 0.5f );
|
|
EXPECT_EQ( CSSPosition::Absolute, jumpToContent->getCSSPosition() );
|
|
EXPECT_NEAR( -865.f, jumpToContent->convertToWorldSpace( { 0, 0 } ).x, 1.f );
|
|
EXPECT_NEAR( 25.f, jumpToContent->convertToWorldSpace( { 0, 0 } ).y, 1.f );
|
|
|
|
Engine::destroySingleton();
|
|
}
|
|
|
|
UTEST( UIHTML, PaddedHeaderWithNestedFloatsDoesNotOverlapClearedMain ) {
|
|
auto win = Engine::instance()->createWindow(
|
|
WindowSettings( 1024, 653, "padded header nested floats", WindowStyle::Default,
|
|
WindowBackend::Default, 32, {}, 1, false, true ),
|
|
ContextSettings( false, 0, 0, GLv_default, true, false ) );
|
|
FileSystem::changeWorkingDirectory( Sys::getProcessPath() );
|
|
|
|
UISceneNode* sceneNode = init_test_inline_block();
|
|
sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( R"html(
|
|
<html>
|
|
<head>
|
|
<style>
|
|
#header {
|
|
padding: 30px 0 0 0;
|
|
width: 940px;
|
|
}
|
|
#masthead {
|
|
width: 940px;
|
|
}
|
|
#branding {
|
|
float: left;
|
|
width: 940px;
|
|
height: 267px;
|
|
}
|
|
#access {
|
|
display: block;
|
|
float: left;
|
|
width: 940px;
|
|
}
|
|
#access ul {
|
|
list-style: none;
|
|
margin: 0;
|
|
padding: 0;
|
|
}
|
|
#access li {
|
|
float: left;
|
|
}
|
|
#access a {
|
|
display: block;
|
|
line-height: 38px;
|
|
padding: 0 10px;
|
|
}
|
|
#main {
|
|
clear: both;
|
|
padding: 40px 0 0 0;
|
|
width: 940px;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div id="header">
|
|
<div id="masthead">
|
|
<div id="branding"></div>
|
|
<div id="access">
|
|
<ul>
|
|
<li><a>Home</a></li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div id="main"></div>
|
|
</body>
|
|
</html>
|
|
)html" ) );
|
|
win->getInput()->update();
|
|
SceneManager::instance()->update();
|
|
sceneNode->updateDirtyLayouts();
|
|
|
|
auto* header = sceneNode->getRoot()->find( "header" )->asType<UIHTMLWidget>();
|
|
auto* main = sceneNode->getRoot()->find( "main" )->asType<UIHTMLWidget>();
|
|
auto* branding = sceneNode->getRoot()->find( "branding" )->asType<UIHTMLWidget>();
|
|
auto* access = sceneNode->getRoot()->find( "access" )->asType<UIHTMLWidget>();
|
|
ASSERT_TRUE( header != nullptr );
|
|
ASSERT_TRUE( main != nullptr );
|
|
ASSERT_TRUE( branding != nullptr );
|
|
ASSERT_TRUE( access != nullptr );
|
|
|
|
const Float headerBottom =
|
|
header->convertToWorldSpace( { 0, 0 } ).y + header->getPixelsSize().getHeight();
|
|
const Float mainTop = main->convertToWorldSpace( { 0, 0 } ).y;
|
|
|
|
EXPECT_GE( header->getPixelsSize().getHeight(), header->getPixelsContentOffset().Top +
|
|
branding->getPixelsSize().getHeight() +
|
|
access->getPixelsSize().getHeight() );
|
|
EXPECT_GE( mainTop + 0.5f, headerBottom );
|
|
|
|
Engine::destroySingleton();
|
|
}
|
|
|
|
UTEST( UIHTML, FlexInfobarAvoidsRightFloat ) {
|
|
auto win = Engine::instance()->createWindow(
|
|
WindowSettings( 800, 600, "flex infobar avoids right float", WindowStyle::Default,
|
|
WindowBackend::Default, 32, {}, 1, false, true ),
|
|
ContextSettings( false, 0, 0, GLv_default, true, false ) );
|
|
FileSystem::changeWorkingDirectory( Sys::getProcessPath() );
|
|
|
|
UISceneNode* sceneNode = init_test_inline_block();
|
|
sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( R"html(
|
|
<html>
|
|
<head>
|
|
<style>
|
|
body {
|
|
margin: 0;
|
|
font: normal 12px verdana;
|
|
}
|
|
.side {
|
|
float: right;
|
|
width: 300px;
|
|
height: 120px;
|
|
margin: 0 5px;
|
|
}
|
|
.content {
|
|
margin: 7px 5px 0 5px;
|
|
}
|
|
.infobar {
|
|
display: flex;
|
|
overflow: hidden;
|
|
box-sizing: border-box;
|
|
height: 124px;
|
|
margin: 5px;
|
|
padding: 0;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="side" id="side"></div>
|
|
<div class="content" id="content">
|
|
<section class="infobar" id="infobar">
|
|
<a>Welcome to Reddit.</a>
|
|
</section>
|
|
</div>
|
|
</body>
|
|
</html>
|
|
)html" ) );
|
|
win->getInput()->update();
|
|
SceneManager::instance()->update();
|
|
sceneNode->updateDirtyLayouts();
|
|
sceneNode->updateDirtyLayouts();
|
|
|
|
auto* side = sceneNode->getRoot()->find( "side" )->asType<UIHTMLWidget>();
|
|
auto* infobar = sceneNode->getRoot()->find( "infobar" )->asType<UIHTMLWidget>();
|
|
ASSERT_TRUE( side != nullptr );
|
|
ASSERT_TRUE( infobar != nullptr );
|
|
|
|
const Float sideLeft = side->convertToWorldSpace( { 0, 0 } ).x;
|
|
const Float infobarRight =
|
|
infobar->convertToWorldSpace( { 0, 0 } ).x + infobar->getPixelsSize().getWidth();
|
|
|
|
EXPECT_LE( infobarRight, sideLeft + 0.5f );
|
|
|
|
Engine::destroySingleton();
|
|
}
|
|
|
|
UTEST( UIHTML, FloatedInlineBlockButtonKeepsTextInside ) {
|
|
auto win = Engine::instance()->createWindow(
|
|
WindowSettings( 800, 600, "floated inline-block button text", WindowStyle::Default,
|
|
WindowBackend::Default, 32, {}, 1, false, true ),
|
|
ContextSettings( false, 0, 0, GLv_default, true, false ) );
|
|
FileSystem::changeWorkingDirectory( Sys::getProcessPath() );
|
|
|
|
UISceneNode* sceneNode = init_test_inline_block();
|
|
sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( R"html(
|
|
<html>
|
|
<head>
|
|
<style>
|
|
body {
|
|
margin: 0;
|
|
font: normal 12px verdana;
|
|
}
|
|
.cta-container {
|
|
overflow: hidden;
|
|
width: 425px;
|
|
}
|
|
.cta-button {
|
|
background-color: #4f86b5;
|
|
color: white;
|
|
display: inline-block;
|
|
float: left;
|
|
font-size: 12px;
|
|
font-weight: bold;
|
|
line-height: 20px;
|
|
margin-right: 12px;
|
|
padding: 4px 12px 3px;
|
|
text-transform: uppercase;
|
|
white-space: nowrap;
|
|
}
|
|
.cta-desc {
|
|
font-size: 12px;
|
|
margin: 0;
|
|
padding-top: 5px;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="cta-container" id="cta">
|
|
<span class="cta-button" id="button">Become a Redditor</span>
|
|
<p class="cta-desc" id="desc">and subscribe to one of thousands of communities.</p>
|
|
</div>
|
|
</body>
|
|
</html>
|
|
)html" ) );
|
|
win->getInput()->update();
|
|
SceneManager::instance()->update();
|
|
sceneNode->updateDirtyLayouts();
|
|
sceneNode->updateDirtyLayouts();
|
|
|
|
auto* button = sceneNode->getRoot()->find( "button" )->asType<UIHTMLWidget>();
|
|
ASSERT_TRUE( button != nullptr );
|
|
auto* buttonRichText = button->getRichTextPtr();
|
|
ASSERT_TRUE( buttonRichText != nullptr );
|
|
ASSERT_EQ( buttonRichText->getLines().size(), (size_t)1 );
|
|
ASSERT_FALSE( buttonRichText->getLines().front().spans.empty() );
|
|
|
|
const auto& textSpan = buttonRichText->getLines().front().spans.front();
|
|
const Rectf padding = button->getPixelsPadding();
|
|
EXPECT_LE( textSpan.position.x + textSpan.size.getWidth(),
|
|
button->getPixelsSize().getWidth() - padding.Left - padding.Right + 0.5f );
|
|
EXPECT_LE( textSpan.position.y + textSpan.size.getHeight(),
|
|
button->getPixelsSize().getHeight() - padding.Top - padding.Bottom + 0.5f );
|
|
|
|
Engine::destroySingleton();
|
|
}
|
|
|
|
UTEST( UIHTML, ListingsSignupCtaTextSitsBesideFloatedButton ) {
|
|
auto win = Engine::instance()->createWindow(
|
|
WindowSettings( 800, 600, "listings signup cta float text", WindowStyle::Default,
|
|
WindowBackend::Default, 32, {}, 1, false, true ),
|
|
ContextSettings( false, 0, 0, GLv_default, true, false ) );
|
|
FileSystem::changeWorkingDirectory( Sys::getProcessPath() );
|
|
|
|
UISceneNode* sceneNode = init_test_inline_block();
|
|
sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( R"html(
|
|
<html>
|
|
<head>
|
|
<style>
|
|
body {
|
|
margin: 0;
|
|
font: normal 12px verdana;
|
|
}
|
|
.infobar {
|
|
align-items: center;
|
|
background-color: #fefbda;
|
|
display: flex;
|
|
height: 124px;
|
|
overflow: hidden;
|
|
width: 780px;
|
|
}
|
|
.container {
|
|
color: black;
|
|
display: block;
|
|
padding: 0 18px;
|
|
width: 55%;
|
|
}
|
|
.title {
|
|
display: inline-block;
|
|
font-size: 20px;
|
|
font-weight: bold;
|
|
line-height: 1;
|
|
margin: 0 0.25em 10px 0;
|
|
}
|
|
.desc {
|
|
display: block;
|
|
font-size: 16px;
|
|
font-weight: bold;
|
|
line-height: 1.1;
|
|
margin: 0 0 14px 0;
|
|
max-width: 425px;
|
|
}
|
|
.cta-container {
|
|
overflow: hidden;
|
|
}
|
|
.cta-button {
|
|
background-color: #4f86b5;
|
|
color: white;
|
|
display: inline-block;
|
|
float: left;
|
|
font-size: 12px;
|
|
font-weight: bold;
|
|
line-height: 20px;
|
|
margin-right: 12px;
|
|
padding: 4px 12px 3px;
|
|
text-transform: uppercase;
|
|
white-space: nowrap;
|
|
}
|
|
.cta-desc {
|
|
font-size: 12px;
|
|
margin: 0;
|
|
padding-top: 5px;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<section class="infobar" id="infobar">
|
|
<a class="container" id="container">
|
|
<h2 class="title">Welcome to Reddit.</h2>
|
|
<p class="desc">Where a community about your favorite things is waiting for you.</p>
|
|
<div class="cta-container" id="cta">
|
|
<span class="cta-button" id="button">Become a Redditor</span>
|
|
<p class="cta-desc" id="cta-desc">and subscribe to one of thousands of communities.</p>
|
|
</div>
|
|
</a>
|
|
</section>
|
|
</body>
|
|
</html>
|
|
)html" ) );
|
|
win->getInput()->update();
|
|
SceneManager::instance()->update();
|
|
sceneNode->updateDirtyLayouts();
|
|
sceneNode->updateDirtyLayouts();
|
|
|
|
auto* cta = sceneNode->getRoot()->find( "cta" )->asType<UIHTMLWidget>();
|
|
auto* button = sceneNode->getRoot()->find( "button" )->asType<UIHTMLWidget>();
|
|
auto* desc = sceneNode->getRoot()->find( "cta-desc" )->asType<UIHTMLWidget>();
|
|
ASSERT_TRUE( cta != nullptr );
|
|
ASSERT_TRUE( button != nullptr );
|
|
ASSERT_TRUE( desc != nullptr );
|
|
auto* buttonRichText = button->getRichTextPtr();
|
|
ASSERT_TRUE( buttonRichText != nullptr );
|
|
ASSERT_EQ( buttonRichText->getLines().size(), (size_t)1 );
|
|
ASSERT_FALSE( buttonRichText->getLines().front().spans.empty() );
|
|
|
|
const Vector2f buttonPos = button->convertToWorldSpace( { 0, 0 } );
|
|
const Vector2f descPos = desc->convertToWorldSpace( { 0, 0 } );
|
|
const auto& buttonTextSpan = buttonRichText->getLines().front().spans.front();
|
|
ASSERT_TRUE( desc->getRichTextPtr() != nullptr );
|
|
ASSERT_FALSE( desc->getRichTextPtr()->getLines().empty() );
|
|
ASSERT_FALSE( desc->getRichTextPtr()->getLines().front().spans.empty() );
|
|
const auto& firstDescSpan = desc->getRichTextPtr()->getLines().front().spans.front();
|
|
|
|
EXPECT_LE( buttonTextSpan.position.x, 1.f );
|
|
EXPECT_LE( buttonTextSpan.position.x + buttonTextSpan.size.getWidth(),
|
|
button->getPixelsSize().getWidth() - button->getPixelsContentOffset().Left -
|
|
button->getPixelsContentOffset().Right + 0.5f );
|
|
EXPECT_GE( descPos.x + firstDescSpan.position.x,
|
|
buttonPos.x + button->getPixelsSize().getWidth() - 0.5f );
|
|
EXPECT_LT( std::abs( descPos.y - buttonPos.y ), button->getPixelsSize().getHeight() );
|
|
|
|
Engine::destroySingleton();
|
|
}
|
|
|
|
UTEST( UIHTML, AnchorsSizing ) {
|
|
auto win = Engine::instance()->createWindow(
|
|
WindowSettings( 1024, 653, "anchors sizing", WindowStyle::Default, WindowBackend::Default,
|
|
32, {}, 1, false, true ),
|
|
ContextSettings( false, 0, 0, GLv_default, true, false ) );
|
|
FileSystem::changeWorkingDirectory( Sys::getProcessPath() );
|
|
|
|
UISceneNode* sceneNode = init_test_inline_block();
|
|
|
|
sceneNode->setURI( "file://" + Sys::getProcessPath() + "assets/html/" );
|
|
|
|
std::string html;
|
|
FileSystem::fileGet( "assets/html/lobsters_simple.html", html );
|
|
sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( html ) );
|
|
win->setClearColor( Color::White );
|
|
|
|
win->getInput()->update();
|
|
SceneManager::instance()->update();
|
|
|
|
win->clear();
|
|
SceneManager::instance()->draw();
|
|
win->display();
|
|
|
|
auto anchors = sceneNode->getRoot()->findAllByTag( "a" );
|
|
|
|
EXPECT_GT( anchors.size(), (size_t)0 );
|
|
|
|
for ( auto anchor : anchors ) {
|
|
auto a = anchor->asType<UIAnchorSpan>();
|
|
if ( a->getDisplay() == CSSDisplay::None )
|
|
continue;
|
|
EXPECT_GT( a->getPixelsSize().getHeight(), 0 );
|
|
if ( !a->getText().empty() && a->getFontStyleConfig().Font ) {
|
|
String text = UIRichText::collapseInternalWhitespace( a->getText() );
|
|
text.trim();
|
|
if ( !text.empty() )
|
|
EXPECT_GE( a->getPixelsSize().getWidth(),
|
|
Text::getTextWidth( text, a->getFontStyleConfig() ) );
|
|
}
|
|
}
|
|
|
|
Engine::destroySingleton();
|
|
}
|
|
|
|
static UISceneNode* createWinAndLoadHTML( std::string winName, std::string htmlPath ) {
|
|
auto win = Engine::instance()->createWindow(
|
|
WindowSettings( 1024, 653, winName, WindowStyle::Default, WindowBackend::Default, 32, {}, 1,
|
|
false, true ),
|
|
ContextSettings( false, 0, 0, GLv_default, true, false ) );
|
|
FileSystem::changeWorkingDirectory( Sys::getProcessPath() );
|
|
|
|
FontTrueType* font = FontTrueType::New( "NotoSans-Regular" ).get();
|
|
font->loadFromFile( "../assets/fonts/NotoSans-Regular.ttf" );
|
|
if ( font == nullptr || !font->loaded() )
|
|
return nullptr;
|
|
FontFamily::loadFromRegular( font );
|
|
|
|
UISceneNode* sceneNode = UISceneNode::New();
|
|
SceneManager::instance()->add( sceneNode );
|
|
UI::UIThemeManager* themeManager = sceneNode->getUIThemeManager();
|
|
themeManager->setDefaultFont( font );
|
|
sceneNode->setURI( "file://" + Sys::getProcessPath() + "assets/html/" );
|
|
std::string html;
|
|
FileSystem::fileGet( htmlPath, html );
|
|
sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( html ) );
|
|
win->setClearColor( Color::White );
|
|
|
|
win->getInput()->update();
|
|
SceneManager::instance()->update();
|
|
|
|
win->clear();
|
|
SceneManager::instance()->draw();
|
|
win->display();
|
|
|
|
return sceneNode;
|
|
}
|
|
|
|
UTEST( UIHTML, blockFlow ) {
|
|
auto sceneNode = createWinAndLoadHTML( "HTML Block Flow", "assets/html/block_flow.html" );
|
|
ASSERT_TRUE( sceneNode != nullptr );
|
|
auto sections = sceneNode->getRoot()->findAllByClass( "language-section" );
|
|
|
|
ASSERT_EQ( sections.size(), (size_t)6 );
|
|
|
|
// Each section is display block so we expect a single section per line
|
|
// if sections position are not equal it means that some sections are floating
|
|
Float ref = sections[0]->getPixelsPosition().x;
|
|
for ( auto section : sections )
|
|
EXPECT_EQ( section->getPixelsPosition().x, ref );
|
|
|
|
Engine::destroySingleton();
|
|
}
|
|
|
|
UTEST( UIHTML, blockFlowFloat ) {
|
|
auto sceneNode =
|
|
createWinAndLoadHTML( "HTML Block Flow", "assets/html/block_flow_float_left.html" );
|
|
ASSERT_TRUE( sceneNode != nullptr );
|
|
auto sections = sceneNode->getRoot()->findAllByClass( "language-section" );
|
|
|
|
ASSERT_EQ( sections.size(), (size_t)6 );
|
|
|
|
// Each section is display block with float: left and width 48% so we expect two sections
|
|
// per line, and each odd index should be to the right
|
|
Float refLeft = sections[0]->getPixelsPosition().x;
|
|
Float refRight = sections[1]->getPixelsPosition().x;
|
|
for ( size_t idx = 0; idx < sections.size(); idx++ ) {
|
|
Float expected = idx % 2 == 0 ? refLeft : refRight;
|
|
EXPECT_EQ( sections[idx]->getPixelsPosition().x, expected );
|
|
}
|
|
Engine::destroySingleton();
|
|
}
|
|
|
|
UTEST( FontTrueType, glyphScaleZeroDimensionsNoCrash ) {
|
|
Engine::instance()->createWindow( WindowSettings( 1024, 650, "Glyph Scale Test",
|
|
WindowStyle::Default, WindowBackend::Default,
|
|
32, {}, 1, false, true ),
|
|
ContextSettings( false, 0, 0, GLv_default, true, false ) );
|
|
FileSystem::changeWorkingDirectory( Sys::getProcessPath() );
|
|
|
|
FontTrueType* font = FontTrueType::New( "NotoSans-Regular" ).get();
|
|
font->loadFromFile( "../assets/fonts/NotoSans-Regular.ttf" );
|
|
ASSERT_TRUE( font != nullptr && font->loaded() );
|
|
FontFamily::loadFromRegular( font );
|
|
|
|
for ( unsigned int size : { 1u, 2u, 3u, 4u, 5u, 6u, 7u, 8u } ) {
|
|
for ( Uint32 ch : { 'A', 'Z', 'a', 'z', '0', '9', '!', '@', '#', '$' } ) {
|
|
const Glyph& glyph = font->getGlyph( ch, size, false, false, 0.f );
|
|
(void)glyph;
|
|
}
|
|
}
|
|
|
|
UISceneNode* sceneNode = UISceneNode::New();
|
|
SceneManager::instance()->add( sceneNode );
|
|
UI::UIThemeManager* themeManager = sceneNode->getUIThemeManager();
|
|
themeManager->setDefaultFont( font );
|
|
|
|
sceneNode->loadLayoutFromString( R"html(
|
|
<vbox layout_width="match_parent" layout_height="match_parent">
|
|
<TextView id="tiny" text="Tiny text" font_size="1" />
|
|
<TextView id="small" text="Small text" font_size="3" />
|
|
<TextView id="normal" text="Normal text" font_size="14" />
|
|
</vbox>
|
|
)html" );
|
|
|
|
sceneNode->updateDirtyLayouts();
|
|
|
|
auto tiny = sceneNode->getRoot()->find( "tiny" );
|
|
auto small = sceneNode->getRoot()->find( "small" );
|
|
auto normal = sceneNode->getRoot()->find( "normal" );
|
|
|
|
ASSERT_TRUE( tiny != nullptr );
|
|
ASSERT_TRUE( small != nullptr );
|
|
ASSERT_TRUE( normal != nullptr );
|
|
|
|
EXPECT_GT( normal->getPixelsSize().getWidth(), 0 );
|
|
EXPECT_GT( normal->getPixelsSize().getHeight(), 0 );
|
|
|
|
Engine::destroySingleton();
|
|
}
|
|
|
|
UTEST( UIHTML, LiFloatLeft ) {
|
|
auto win = Engine::instance()->createWindow(
|
|
WindowSettings( 1024, 653, "li float left", WindowStyle::Default, WindowBackend::Default,
|
|
32, {}, 1, false, true ),
|
|
ContextSettings( false, 0, 0, GLv_default, true, false ) );
|
|
FileSystem::changeWorkingDirectory( Sys::getProcessPath() );
|
|
|
|
UISceneNode* sceneNode = init_test_inline_block();
|
|
|
|
sceneNode->setURI( "file://" + Sys::getProcessPath() + "assets/html/" );
|
|
|
|
std::string html;
|
|
FileSystem::fileGet( "assets/html/float_li.html", html );
|
|
sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( html ) );
|
|
win->setClearColor( Color::White );
|
|
|
|
win->getInput()->update();
|
|
SceneManager::instance()->update();
|
|
|
|
win->clear();
|
|
SceneManager::instance()->draw();
|
|
win->display();
|
|
|
|
auto livec = sceneNode->getRoot()->findAllByTag( "li" );
|
|
|
|
ASSERT_GT( livec.size(), (size_t)0 );
|
|
|
|
auto refY = livec[0]->getPixelsPosition().y;
|
|
|
|
for ( size_t i = 1; i < livec.size(); i++ )
|
|
EXPECT_NEAR( refY, livec[i]->getPixelsPosition().y, 1.f );
|
|
|
|
Engine::destroySingleton();
|
|
}
|
|
|
|
UTEST( UIHTML, FlexCenterWebViewLikeLayout ) {
|
|
Engine::instance()->createWindow( WindowSettings( 1280, 720, "Flex Center WebView",
|
|
WindowStyle::Default, WindowBackend::Default,
|
|
32, {}, 1, false, true ),
|
|
ContextSettings( false, 0, 0, GLv_default, true, false ) );
|
|
|
|
UISceneNode* sceneNode = init_test_inline_block();
|
|
sceneNode->setURI( "file://" + Sys::getProcessPath() + "assets/html/" );
|
|
|
|
std::string html;
|
|
FileSystem::fileGet( "assets/html/flex_center.html", html );
|
|
sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( html ) );
|
|
|
|
sceneNode->update( Seconds( 1 ) );
|
|
sceneNode->updateDirtyLayouts();
|
|
|
|
auto postWrapper = sceneNode->getRoot()->findByClass( "post-wrapper" );
|
|
auto post = sceneNode->getRoot()->findByClass( "post" );
|
|
ASSERT_TRUE( postWrapper != nullptr );
|
|
ASSERT_TRUE( post != nullptr );
|
|
|
|
auto postWidget = post->asType<UIWidget>();
|
|
|
|
// In a real browser a div with width:100% inside a column flex container
|
|
// with align-items:center still spans the full cross axis because 100%
|
|
// resolves against the container. The item should therefore sit flush with
|
|
// the container's left edge (plus any container padding).
|
|
EXPECT_NEAR( postWidget->getPixelsPosition().x, 0.f, 2.f );
|
|
|
|
Engine::destroySingleton();
|
|
}
|
|
|
|
UTEST( UIHTML, FlexCenterNoTextNodeDisplacement ) {
|
|
Engine::instance()->createWindow( WindowSettings( 1024, 768, "Flex Center Test",
|
|
WindowStyle::Default, WindowBackend::Default,
|
|
32, {}, 1, false, true ),
|
|
ContextSettings( false, 0, 0, GLv_default, true, false ) );
|
|
|
|
UISceneNode* sceneNode = init_test_inline_block();
|
|
sceneNode->setURI( "file://" + Sys::getProcessPath() + "assets/html/" );
|
|
|
|
std::string html;
|
|
FileSystem::fileGet( "assets/html/flex_center.html", html );
|
|
sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( html ) );
|
|
|
|
sceneNode->update( Seconds( 1 ) );
|
|
sceneNode->updateDirtyLayouts();
|
|
|
|
auto postWrapper = sceneNode->getRoot()->findByClass( "post-wrapper" );
|
|
ASSERT_TRUE( postWrapper != nullptr );
|
|
auto post = postWrapper->findByClass( "post" );
|
|
ASSERT_TRUE( post != nullptr );
|
|
|
|
// The post-wrapper is a column flex container. Whitespace text nodes
|
|
// between the tags must not be treated as flex items, otherwise the
|
|
// .post child would be displaced downward.
|
|
auto postWrapperWidget = postWrapper->asType<UIWidget>();
|
|
auto postWidget = post->asType<UIWidget>();
|
|
|
|
Float postX = postWidget->getPixelsPosition().x;
|
|
Float postY = postWidget->getPixelsPosition().y;
|
|
Float wrapperTopPadding = postWrapperWidget->getPixelsPadding().Top;
|
|
Float postMarginTop = postWidget->getLayoutPixelsMargin().Top;
|
|
Float wrapperW = postWrapperWidget->getPixelsSize().getWidth();
|
|
Float postW = postWidget->getPixelsSize().getWidth();
|
|
|
|
// In a column flex container the first (and only) real flex item should
|
|
// sit right after the container's top padding plus its own margin-top.
|
|
EXPECT_NEAR( postY, wrapperTopPadding + postMarginTop, 2.f );
|
|
|
|
// The .post div has width: 100% so it should span the full cross axis.
|
|
// With align-items: center there is no free space, so x should be at the
|
|
// container's left padding (not displaced toward the right edge).
|
|
EXPECT_NEAR( postX, 0.f, 2.f );
|
|
EXPECT_GT( postW, wrapperW * 0.5f );
|
|
|
|
Engine::destroySingleton();
|
|
}
|
|
|
|
UTEST( UIHTML, BlockSizeInfDoesNotHang ) {
|
|
Engine::instance()->createWindow( WindowSettings( 1024, 768, "Block Size Inf Test",
|
|
WindowStyle::Default, WindowBackend::Default,
|
|
32, {}, 1, false, true ),
|
|
ContextSettings( false, 0, 0, GLv_default, true, false ) );
|
|
|
|
UISceneNode* sceneNode = init_test_inline_block();
|
|
sceneNode->setURI( "file://" + Sys::getProcessPath() + "assets/html/" );
|
|
|
|
std::string html;
|
|
FileSystem::fileGet( "assets/html/block_size_inf.html", html );
|
|
sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( html ) );
|
|
|
|
sceneNode->update( Seconds( 1 ) );
|
|
sceneNode->updateDirtyLayouts();
|
|
|
|
auto body = sceneNode->getRoot()->findByTag( "body" );
|
|
ASSERT_TRUE( body != nullptr );
|
|
auto bodyWidget = body->asType<UIWidget>();
|
|
EXPECT_GT( bodyWidget->getPixelsSize().getHeight(), 0.f );
|
|
|
|
auto* logoNode = sceneNode->getRoot()->findByClass( "logo" );
|
|
ASSERT_TRUE( logoNode != nullptr );
|
|
auto* logo = logoNode->asType<UIWidget>();
|
|
auto* logoLink = logo->getParent()->asType<UIWidget>();
|
|
ASSERT_TRUE( logoLink != nullptr );
|
|
EXPECT_GT( logoLink->getPixelsSize().getWidth(), 0.f );
|
|
EXPECT_NEAR( logoLink->getPixelsSize().getWidth(), logo->getPixelsSize().getWidth(), 1.f );
|
|
EXPECT_LT( logoLink->getPixelsSize().getWidth(), 1024.f );
|
|
|
|
Engine::destroySingleton();
|
|
}
|
|
|
|
UTEST( UIHTML, WebViewAsyncInlineImageAutoMarginsResolveToZero ) {
|
|
Engine::instance()->createWindow( WindowSettings( 2048, 1152, "Image Anchor Bounds Test",
|
|
WindowStyle::Default, WindowBackend::Default,
|
|
32, {}, 1, false, true ),
|
|
ContextSettings( false, 0, 0, GLv_default, true, false ) );
|
|
FileSystem::changeWorkingDirectory( Sys::getProcessPath() );
|
|
|
|
auto* win = Engine::instance()->getCurrentWindow();
|
|
UISceneNode* sceneNode = init_test_inline_block();
|
|
sceneNode->setThreadPool( ThreadPool::createShared( 4 ) );
|
|
auto* host = sceneNode->loadLayoutFromString( R"xml(
|
|
<vbox layout_width="match_parent" layout_height="match_parent">
|
|
<hbox layout_width="match_parent" layout_height="wrap_content">
|
|
<TextInput layout_width="0" layout_weight="1" />
|
|
</hbox>
|
|
<WebView id="webview" layout_width="match_parent" layout_height="0" layout_weight="1" />
|
|
</vbox>
|
|
)xml" );
|
|
auto* webView = host->find( "webview" )->asType<UIWebView>();
|
|
webView->loadURI( URI( "./assets/html/block_size_inf.html" ) );
|
|
|
|
UISceneNode* documentScene = webView->getDocumentSceneNode();
|
|
ASSERT_TRUE( documentScene != nullptr );
|
|
UIWidget* logo = nullptr;
|
|
for ( int i = 0; i < 200; ++i ) {
|
|
win->getInput()->update();
|
|
SceneManager::instance()->update( Milliseconds( 16 ) );
|
|
logo = documentScene->getRoot()->findByClass( "logo" );
|
|
Sys::sleep( Milliseconds( 1 ) );
|
|
}
|
|
|
|
ASSERT_TRUE( logo != nullptr );
|
|
auto* logoLink = logo->getParent()->asType<UIWidget>();
|
|
ASSERT_TRUE( logoLink != nullptr );
|
|
EXPECT_GT( logoLink->getPixelsSize().getWidth(), 0.f );
|
|
EXPECT_NEAR( logoLink->getPixelsSize().getWidth(), logo->getPixelsSize().getWidth(), 1.f );
|
|
EXPECT_LT( logoLink->getPixelsSize().getWidth(), webView->getPixelsSize().getWidth() );
|
|
|
|
Engine::destroySingleton();
|
|
}
|
|
|
|
UTEST( UIHTML, FormattingRoleUsedMarginsAreNonMutating ) {
|
|
auto win = Engine::instance()->createWindow(
|
|
WindowSettings( 640, 480, "CSS used margin roles", WindowStyle::Default,
|
|
WindowBackend::Default, 32, {}, 1, false, true ),
|
|
ContextSettings( false, 0, 0, GLv_default, true, false ) );
|
|
UISceneNode* sceneNode = init_test_inline_block();
|
|
auto* parent = UIHTMLWidget::New();
|
|
parent->setParent( sceneNode->getRoot() );
|
|
parent->setPixelsSize( 400.f, 200.f );
|
|
parent->setLayoutSizePolicy( SizePolicy::Fixed, SizePolicy::Fixed );
|
|
auto* image = UIHTMLImage::New();
|
|
image->setParent( parent );
|
|
image->setPixelsSize( 100.f, 40.f );
|
|
image->setLayoutSizePolicy( SizePolicy::Fixed, SizePolicy::Fixed );
|
|
image->setLayoutPixelsMargin( { 23.f, 17.f, 29.f, 19.f } );
|
|
image->setLayoutMarginAuto( true, true, true, true );
|
|
|
|
EXPECT_EQ( image->getFormattingRole(), CSSFormattingRole::Inline );
|
|
auto used = image->resolveUsedMargins();
|
|
EXPECT_TRUE( used.value == Rectf::Zero );
|
|
EXPECT_TRUE( image->getLayoutPixelsMargin() == Rectf( 23.f, 17.f, 29.f, 19.f ) );
|
|
|
|
image->setDisplay( CSSDisplay::Block );
|
|
EXPECT_EQ( image->getFormattingRole(), CSSFormattingRole::NormalFlowBlock );
|
|
used = image->resolveUsedMargins();
|
|
EXPECT_NEAR( used.value.Left, 150.f, 0.01f );
|
|
EXPECT_NEAR( used.value.Right, 150.f, 0.01f );
|
|
EXPECT_EQ( used.value.Top, 0.f );
|
|
EXPECT_EQ( used.value.Bottom, 0.f );
|
|
|
|
image->setCSSFloat( CSSFloat::Left );
|
|
EXPECT_EQ( image->getFormattingRole(), CSSFormattingRole::Float );
|
|
EXPECT_TRUE( image->resolveUsedMargins().value == Rectf::Zero );
|
|
image->setCSSFloat( CSSFloat::None );
|
|
image->setCSSPosition( CSSPosition::Absolute );
|
|
EXPECT_EQ( image->getFormattingRole(), CSSFormattingRole::Absolute );
|
|
EXPECT_TRUE( image->resolveUsedMargins().value == Rectf::Zero );
|
|
image->setCSSPosition( CSSPosition::Static );
|
|
|
|
parent->setDisplay( CSSDisplay::Flex );
|
|
EXPECT_EQ( image->getFormattingRole(), CSSFormattingRole::FlexItem );
|
|
EXPECT_TRUE( image->resolveUsedMargins().value == Rectf::Zero );
|
|
parent->setDisplay( CSSDisplay::Grid );
|
|
EXPECT_EQ( image->getFormattingRole(), CSSFormattingRole::GridItem );
|
|
EXPECT_TRUE( image->resolveUsedMargins().value == Rectf::Zero );
|
|
|
|
(void)win;
|
|
Engine::destroySingleton();
|
|
}
|
|
|
|
UTEST( UIHTML, KittyHomeSmallDoesNotHang ) {
|
|
Engine::instance()->createWindow( WindowSettings( 1024, 768, "Kitty Home Small Test",
|
|
WindowStyle::Default, WindowBackend::Default,
|
|
32, {}, 1, false, true ),
|
|
ContextSettings( false, 0, 0, GLv_default, true, false ) );
|
|
|
|
UISceneNode* sceneNode = init_test_inline_block();
|
|
sceneNode->setURI( "file://" + Sys::getProcessPath() + "assets/html/" );
|
|
|
|
std::string html;
|
|
FileSystem::fileGet( "assets/html/kitty_home_small.html", html );
|
|
ASSERT_FALSE( html.empty() );
|
|
sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( html ) );
|
|
|
|
sceneNode->update( Seconds( 1 ) );
|
|
sceneNode->updateDirtyLayouts();
|
|
|
|
auto body = sceneNode->getRoot()->findByTag( "body" );
|
|
ASSERT_TRUE( body != nullptr );
|
|
auto bodyWidget = body->asType<UIWidget>();
|
|
EXPECT_GT( bodyWidget->getPixelsSize().getWidth(), 0.f );
|
|
EXPECT_GT( bodyWidget->getPixelsSize().getHeight(), 0.f );
|
|
|
|
Engine::destroySingleton();
|
|
}
|
|
|
|
UTEST( UIHTML, FlexFormLayout ) {
|
|
Engine::instance()->createWindow( WindowSettings( 1024, 653, "Flex Form Layout Test",
|
|
WindowStyle::Default, WindowBackend::Default,
|
|
32, {}, 1, false, true ),
|
|
ContextSettings( false, 0, 0, GLv_default, true, false ) );
|
|
|
|
UISceneNode* sceneNode = init_test_inline_block();
|
|
sceneNode->setURI( "file://" + Sys::getProcessPath() + "assets/html/" );
|
|
|
|
std::string html;
|
|
FileSystem::fileGet( "assets/html/flex_form.html", html );
|
|
sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( html ) );
|
|
|
|
sceneNode->update( Seconds( 1 ) );
|
|
sceneNode->updateDirtyLayouts();
|
|
|
|
auto form = sceneNode->getRoot()->findByClass( "newsletter-form" );
|
|
ASSERT_TRUE( form != nullptr );
|
|
auto formWidget = form->asType<UIWidget>();
|
|
|
|
// The form has display: flex with no explicit height; it must derive
|
|
// its height from the tallest flex item (the input or button).
|
|
EXPECT_GT( formWidget->getPixelsSize().getHeight(), 0.f );
|
|
|
|
// Find the email input and subscribe button within the form.
|
|
auto input = formWidget->findByTag( "input" );
|
|
auto button = formWidget->findByTag( "button" );
|
|
ASSERT_TRUE( input != nullptr );
|
|
ASSERT_TRUE( button != nullptr );
|
|
auto inputWidget = input->asType<UIWidget>();
|
|
auto buttonWidget = button->asType<UIWidget>();
|
|
|
|
// In a row flex container the button should sit to the right of the input.
|
|
Float inputRight = inputWidget->getPixelsPosition().x + inputWidget->getPixelsSize().getWidth();
|
|
EXPECT_GT( buttonWidget->getPixelsPosition().x, inputRight - 1.f );
|
|
|
|
auto* buttonRichText = buttonWidget->isType( UI_TYPE_RICHTEXT )
|
|
? buttonWidget->asType<UIRichText>()->getRichTextPtr()
|
|
: nullptr;
|
|
ASSERT_TRUE( buttonRichText != nullptr );
|
|
buttonRichText->updateLayout();
|
|
EXPECT_EQ( buttonRichText->getLines().size(), 1u );
|
|
|
|
Engine::destroySingleton();
|
|
}
|
|
|
|
UTEST( UIHTML, FlexMediaQueriesLayout ) {
|
|
Engine::instance()->createWindow( WindowSettings( 1024, 768, "Flex Media Queries Layout Test",
|
|
WindowStyle::Default, WindowBackend::Default,
|
|
32, {}, 1, false, true ),
|
|
ContextSettings( false, 0, 0, GLv_default, true, false ) );
|
|
|
|
UISceneNode* sceneNode = init_test_inline_block();
|
|
sceneNode->setURI( "file://" + Sys::getProcessPath() + "assets/html/" );
|
|
|
|
std::string html;
|
|
FileSystem::fileGet( "assets/html/flex_mediaqueries.html", html );
|
|
sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( html ) );
|
|
|
|
sceneNode->update( Seconds( 1 ) );
|
|
sceneNode->updateDirtyLayouts();
|
|
|
|
auto body = sceneNode->getRoot()->findByTag( "body" );
|
|
ASSERT_TRUE( body != nullptr );
|
|
auto bodyWidget = body->asType<UIWidget>();
|
|
|
|
// Header
|
|
auto header = bodyWidget->findByClass( "site-header" );
|
|
ASSERT_TRUE( header != nullptr );
|
|
auto headerWidget = header->asType<UIWidget>();
|
|
|
|
// Header logo (contains SVG + "Causality" text)
|
|
auto logo = headerWidget->findByClass( "header-logo" );
|
|
ASSERT_TRUE( logo != nullptr );
|
|
auto logoWidget = logo->asType<UIWidget>();
|
|
|
|
// Header wordmark (the "Causality" span)
|
|
auto wordmark = headerWidget->findByClass( "header-wordmark" );
|
|
ASSERT_TRUE( wordmark != nullptr );
|
|
auto wordmarkWidget = wordmark->asType<UIWidget>();
|
|
|
|
// Site nav
|
|
auto nav = headerWidget->findByClass( "site-nav" );
|
|
ASSERT_TRUE( nav != nullptr );
|
|
auto navWidget = nav->asType<UIWidget>();
|
|
|
|
// Header should have visible height (padding + content + padding)
|
|
EXPECT_GT( headerWidget->getPixelsSize().getHeight(), 80.f );
|
|
|
|
// Header should not exceed content width significantly
|
|
EXPECT_LE( headerWidget->getPixelsSize().getWidth(), 1024.f );
|
|
|
|
// The wordmark "Causality" should have positive width and height (visible text)
|
|
EXPECT_GT( wordmarkWidget->getPixelsSize().getWidth(), 10.f );
|
|
EXPECT_GT( wordmarkWidget->getPixelsSize().getHeight(), 5.f );
|
|
|
|
// The logo (flex item in header) should not extend outside the header
|
|
EXPECT_LE( logoWidget->getPixelsPosition().x + logoWidget->getPixelsSize().getWidth(),
|
|
headerWidget->getPixelsPosition().x + headerWidget->getPixelsSize().getWidth() +
|
|
1.f );
|
|
|
|
// Site nav should not overflow the header
|
|
EXPECT_LE( navWidget->getPixelsPosition().x + navWidget->getPixelsSize().getWidth(),
|
|
headerWidget->getPixelsPosition().x + headerWidget->getPixelsSize().getWidth() +
|
|
1.f );
|
|
|
|
// Essay nav
|
|
auto essayNav = bodyWidget->findByClass( "essay-nav" );
|
|
ASSERT_TRUE( essayNav != nullptr );
|
|
auto essayNavWidget = essayNav->asType<UIWidget>();
|
|
|
|
// The essay-nav link (<a>)
|
|
auto essayNavLink = essayNavWidget->findByClass( "essay-nav-prev" );
|
|
ASSERT_TRUE( essayNavLink != nullptr );
|
|
auto essayNavLinkWidget = essayNavLink->asType<UIWidget>();
|
|
|
|
// essay-nav should contain its link (link should not overflow in local coords)
|
|
Float linkLocalX = essayNavLinkWidget->getPixelsPosition().x;
|
|
Float linkLocalY = essayNavLinkWidget->getPixelsPosition().y;
|
|
Float linkLocalW = essayNavLinkWidget->getPixelsSize().getWidth();
|
|
Float linkLocalH = essayNavLinkWidget->getPixelsSize().getHeight();
|
|
Float navW = essayNavWidget->getPixelsSize().getWidth();
|
|
Float navH = essayNavWidget->getPixelsSize().getHeight();
|
|
|
|
EXPECT_GE( linkLocalX, -1.f );
|
|
EXPECT_LE( linkLocalX + linkLocalW, navW + 1.f );
|
|
EXPECT_GE( linkLocalY, -1.f );
|
|
EXPECT_LE( linkLocalY + linkLocalH, navH + 1.f );
|
|
|
|
// essay-nav should have visible size
|
|
EXPECT_GT( essayNavWidget->getPixelsSize().getHeight(), 10.f );
|
|
EXPECT_GT( essayNavWidget->getPixelsSize().getWidth(), 10.f );
|
|
|
|
// Valid input types that use the text implementation must retain their state for attribute
|
|
// selectors. Collapsing "email" to "text" loses this rule and exposes the white UA default.
|
|
auto* newsletter = bodyWidget->findByClass( "newsletter-form" );
|
|
ASSERT_TRUE( newsletter != nullptr );
|
|
auto* emailInput = newsletter->findByTag( "input" )->asType<UIHTMLInput>();
|
|
ASSERT_TRUE( emailInput != nullptr );
|
|
EXPECT_STDSTREQ( emailInput->getInputType(), "email" );
|
|
EXPECT_TRUE( emailInput->getBackgroundColor() == Color( "#1C1917" ) );
|
|
auto* emailImplementation = emailInput->getChildWidget();
|
|
ASSERT_TRUE( emailImplementation != nullptr );
|
|
const Rectf emailContentOffset = emailInput->getPixelsContentOffset();
|
|
EXPECT_NEAR( emailImplementation->getPixelsSize().getWidth(),
|
|
emailInput->getPixelsSize().getWidth() - emailContentOffset.Left -
|
|
emailContentOffset.Right,
|
|
0.01f );
|
|
EXPECT_NEAR( emailImplementation->getPixelsSize().getHeight(),
|
|
emailInput->getPixelsSize().getHeight() - emailContentOffset.Top -
|
|
emailContentOffset.Bottom,
|
|
0.01f );
|
|
|
|
// The essay-nav link contains two spans: label and title
|
|
// They should stack vertically (flex-direction: column on the <a>)
|
|
// so the link height should be at least the sum of both span heights
|
|
auto essayLabel = essayNavLinkWidget->findByClass( "essay-nav-label" );
|
|
auto essayTitle = essayNavLinkWidget->findByClass( "essay-nav-title" );
|
|
if ( essayLabel && essayTitle ) {
|
|
auto labelWidget = essayLabel->asType<UIWidget>();
|
|
auto titleWidget = essayTitle->asType<UIWidget>();
|
|
EXPECT_GT( essayNavLinkWidget->getPixelsSize().getHeight(),
|
|
labelWidget->getPixelsSize().getHeight() +
|
|
titleWidget->getPixelsSize().getHeight() - 1.f );
|
|
|
|
auto* labelRichText = essayLabel->isType( UI_TYPE_TEXTSPAN )
|
|
? essayLabel->asType<UITextSpan>()->getRichTextPtr()
|
|
: nullptr;
|
|
auto* titleRichText = essayTitle->isType( UI_TYPE_TEXTSPAN )
|
|
? essayTitle->asType<UITextSpan>()->getRichTextPtr()
|
|
: nullptr;
|
|
ASSERT_TRUE( labelRichText != nullptr );
|
|
ASSERT_TRUE( titleRichText != nullptr );
|
|
labelRichText->updateLayout();
|
|
titleRichText->updateLayout();
|
|
EXPECT_EQ( labelRichText->getLines().size(), 1u );
|
|
EXPECT_EQ( titleRichText->getLines().size(), 1u );
|
|
}
|
|
|
|
Engine::destroySingleton();
|
|
}
|
|
|
|
UTEST( UIHTML, FlexAnchorInFlexNavVisible ) {
|
|
Engine::instance()->createWindow( WindowSettings( 1024, 768, "Flex Anchor in Flex Nav Test",
|
|
WindowStyle::Default, WindowBackend::Default,
|
|
32, {}, 1, false, true ),
|
|
ContextSettings( false, 0, 0, GLv_default, true, false ) );
|
|
|
|
UISceneNode* sceneNode = init_test_inline_block();
|
|
sceneNode->setURI( "file://" + Sys::getProcessPath() + "assets/html/" );
|
|
|
|
std::string html;
|
|
FileSystem::fileGet( "assets/html/flex_mediaquery.html", html );
|
|
sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( html ) );
|
|
|
|
sceneNode->update( Seconds( 1 ) );
|
|
sceneNode->updateDirtyLayouts();
|
|
|
|
auto body = sceneNode->getRoot()->findByTag( "body" );
|
|
ASSERT_TRUE( body != nullptr );
|
|
auto bodyWidget = body->asType<UIWidget>();
|
|
|
|
auto nav = bodyWidget->findByClass( "site-nav" );
|
|
ASSERT_TRUE( nav != nullptr );
|
|
auto navWidget = nav->asType<UIWidget>();
|
|
|
|
// The nav is a flex container; its <a> children are blockified flex items.
|
|
// Each <a> should have non-zero width and height (text must be visible).
|
|
for ( Uint32 i = 0; i < navWidget->getChildCount(); ++i ) {
|
|
auto c = navWidget->getChildAt( i );
|
|
auto cw = c->asType<UIWidget>();
|
|
if ( !cw || cw->getElementTag() != "a" )
|
|
continue;
|
|
EXPECT_GT( cw->getPixelsSize().getWidth(), 5.f );
|
|
EXPECT_GT( cw->getPixelsSize().getHeight(), 5.f );
|
|
}
|
|
|
|
Engine::destroySingleton();
|
|
}
|
|
|
|
UTEST( UIHTML, FlexTextSpanWrapContentUsesItemWidth ) {
|
|
Engine::instance()->createWindow( WindowSettings( 1280, 720, "Flex Text Span Width Test",
|
|
WindowStyle::Default, WindowBackend::Default,
|
|
32, {}, 1, false, true ),
|
|
ContextSettings( false, 0, 0, GLv_default, true, false ) );
|
|
|
|
UISceneNode* sceneNode = init_test_inline_block();
|
|
sceneNode->setURI( "file://" + Sys::getProcessPath() + "assets/html/" );
|
|
|
|
std::string html;
|
|
FileSystem::fileGet( "assets/html/newsblur_home_prices_small.html", html );
|
|
sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( html ) );
|
|
|
|
sceneNode->update( Seconds( 1 ) );
|
|
sceneNode->updateDirtyLayouts();
|
|
|
|
auto priceAmount = sceneNode->getRoot()->findByClass( "NB-pricing-tier-price-amount" );
|
|
ASSERT_TRUE( priceAmount != nullptr );
|
|
ASSERT_TRUE( priceAmount->isType( UI_TYPE_TEXTSPAN ) );
|
|
|
|
auto* amountSpan = priceAmount->asType<UITextSpan>();
|
|
auto* amountRichText = amountSpan->getRichTextPtr();
|
|
ASSERT_TRUE( amountRichText != nullptr );
|
|
amountRichText->updateLayout();
|
|
|
|
const auto& lines = amountRichText->getLines();
|
|
ASSERT_EQ( lines.size(), 1u );
|
|
ASSERT_EQ( lines.front().spans.size(), 1u );
|
|
ASSERT_EQ( lines.front().spans.front().type, RichText::RenderSpan::Type::Text );
|
|
ASSERT_TRUE( lines.front().spans.front().text != nullptr );
|
|
EXPECT_STRINGEQ( lines.front().spans.front().text->getString(), "$36" );
|
|
EXPECT_NEAR( lines.front().spans.front().position.x, 0.f, 1.f );
|
|
EXPECT_NEAR( amountSpan->getPixelsSize().getWidth(), lines.front().spans.front().size.x, 2.f );
|
|
|
|
Engine::destroySingleton();
|
|
}
|
|
|
|
UTEST( UIHTML, FlexLiItemsWrapContentWidth ) {
|
|
Engine::instance()->createWindow( WindowSettings( 1024, 768, "Flex LI Width Test",
|
|
WindowStyle::Default, WindowBackend::Default,
|
|
32, {}, 1, false, true ),
|
|
ContextSettings( false, 0, 0, GLv_default, true, false ) );
|
|
|
|
UISceneNode* sceneNode = init_test_inline_block();
|
|
sceneNode->setURI( "file://" + Sys::getProcessPath() + "assets/html/" );
|
|
|
|
std::string html;
|
|
FileSystem::fileGet( "assets/html/flex_li_width.html", html );
|
|
sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( html ) );
|
|
|
|
sceneNode->update( Seconds( 1 ) );
|
|
sceneNode->updateDirtyLayouts();
|
|
|
|
auto menuContainer = sceneNode->getRoot()->findByClass( "menu-container" );
|
|
ASSERT_TRUE( menuContainer != nullptr );
|
|
auto menuWidget = menuContainer->asType<UIWidget>();
|
|
|
|
// The menu container is a flex row; its <li> children should wrap to
|
|
// their content width, not span the full container width.
|
|
Float containerWidth = menuWidget->getPixelsSize().getWidth();
|
|
Float containerPadding =
|
|
menuWidget->getPixelsPadding().Left + menuWidget->getPixelsPadding().Right;
|
|
Float contentWidth = containerWidth - containerPadding;
|
|
|
|
Float totalLiWidth = 0.f;
|
|
Uint32 liCount = 0;
|
|
for ( Uint32 i = 0; i < menuWidget->getChildCount(); ++i ) {
|
|
auto c = menuWidget->getChildAt( i );
|
|
if ( !c->isWidget() )
|
|
continue;
|
|
auto cw = c->asType<UIWidget>();
|
|
if ( cw->getElementTag() != "li" )
|
|
continue;
|
|
|
|
// Each <li> should be narrower than the container content width
|
|
EXPECT_LT( cw->getPixelsSize().getWidth(), contentWidth );
|
|
|
|
// Accumulate total width (including margin)
|
|
Rectf margin = cw->getLayoutPixelsMargin();
|
|
totalLiWidth += cw->getPixelsSize().getWidth() + margin.Left + margin.Right;
|
|
liCount++;
|
|
}
|
|
|
|
ASSERT_GT( liCount, (Uint32)0 );
|
|
// With 9 items and horizontal margins, the total should still fit without
|
|
// each item spanning the full container.
|
|
EXPECT_LT( totalLiWidth, contentWidth * (Float)liCount );
|
|
|
|
Engine::destroySingleton();
|
|
}
|
|
|
|
UTEST( UIHTML, FlexStretchPreservesAutoCrossSizeAfterChildLayout ) {
|
|
Engine::instance()->createWindow(
|
|
WindowSettings( 1024, 768, "Flex Stretch Auto Cross Size Test", WindowStyle::Default,
|
|
WindowBackend::Default, 32, {}, 1, false, true ),
|
|
ContextSettings( false, 0, 0, GLv_default, true, false ) );
|
|
|
|
UISceneNode* sceneNode = init_test_inline_block();
|
|
sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( R"html(
|
|
<html><head><style>
|
|
.container { display: flex; width: 600px; }
|
|
.short { width: 200px; padding: 10px; }
|
|
.tall { width: 200px; height: 300px; }
|
|
</style></head><body>
|
|
<div class="container">
|
|
<aside class="short"><p>Short content</p></aside>
|
|
<main class="tall"></main>
|
|
</div>
|
|
</body></html>
|
|
)html" ) );
|
|
|
|
sceneNode->update( Seconds( 1 ) );
|
|
sceneNode->updateDirtyLayouts();
|
|
|
|
auto* containerNode = sceneNode->getRoot()->findByClass( "container" );
|
|
auto* shortItemNode = sceneNode->getRoot()->findByClass( "short" );
|
|
auto* tallItemNode = sceneNode->getRoot()->findByClass( "tall" );
|
|
ASSERT_TRUE( containerNode != nullptr );
|
|
ASSERT_TRUE( shortItemNode != nullptr );
|
|
ASSERT_TRUE( tallItemNode != nullptr );
|
|
auto* container = containerNode->asType<UIWidget>();
|
|
auto* shortItem = shortItemNode->asType<UIWidget>();
|
|
auto* tallItem = tallItemNode->asType<UIWidget>();
|
|
|
|
EXPECT_NEAR( container->getPixelsSize().getHeight(), tallItem->getPixelsSize().getHeight(),
|
|
1.f );
|
|
EXPECT_NEAR( shortItem->getPixelsSize().getHeight(), tallItem->getPixelsSize().getHeight(),
|
|
1.f );
|
|
|
|
Engine::destroySingleton();
|
|
}
|
|
|
|
UTEST( UIHTML, ImagePercentageWidthRespectsParentMaxWidth ) {
|
|
auto win = Engine::instance()->createWindow(
|
|
WindowSettings( 1024, 768, "img pct width respects parent max-width", WindowStyle::Default,
|
|
WindowBackend::Default, 32, {}, 1, false, true ),
|
|
ContextSettings( false, 0, 0, GLv_default, true, false ) );
|
|
|
|
UISceneNode* sceneNode = init_test_inline_block();
|
|
sceneNode->setURI( "file://" + Sys::getProcessPath() + "assets/html/" );
|
|
|
|
std::string html;
|
|
FileSystem::fileGet( "assets/html/image_width.html", html );
|
|
sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( html ) );
|
|
|
|
win->getInput()->update();
|
|
SceneManager::instance()->update();
|
|
sceneNode->updateDirtyLayouts();
|
|
|
|
auto articles = sceneNode->getRoot()->findAllByTag( "article" );
|
|
ASSERT_GT( articles.size(), (size_t)0 );
|
|
auto* articleWidget = articles[0]->asType<UIWidget>();
|
|
ASSERT_TRUE( articleWidget != nullptr );
|
|
|
|
auto images = sceneNode->getRoot()->findAllByTag( "img" );
|
|
ASSERT_GT( images.size(), (size_t)0 );
|
|
auto* imgWidget = images[0]->asType<UIWidget>();
|
|
ASSERT_TRUE( imgWidget != nullptr );
|
|
|
|
EXPECT_GT( articleWidget->getPixelsSize().getWidth(), 0.f );
|
|
EXPECT_GT( articleWidget->getPixelsSize().getHeight(), 0.f );
|
|
EXPECT_LE( articleWidget->getPixelsSize().getWidth(), 474.f );
|
|
|
|
EXPECT_GT( imgWidget->getPixelsSize().getWidth(), 0.f );
|
|
EXPECT_GT( imgWidget->getPixelsSize().getHeight(), 0.f );
|
|
EXPECT_LE( imgWidget->getPixelsSize().getWidth(), articleWidget->getPixelsSize().getWidth() );
|
|
|
|
auto anchors = sceneNode->getRoot()->findAllByTag( "a" );
|
|
ASSERT_GT( anchors.size(), (size_t)0 );
|
|
auto* anchorWidget = anchors[0]->asType<UIWidget>();
|
|
ASSERT_TRUE( anchorWidget != nullptr );
|
|
EXPECT_GT( anchorWidget->getPixelsSize().getWidth(), 0.f );
|
|
EXPECT_GT( anchorWidget->getPixelsSize().getHeight(), 0.f );
|
|
|
|
Engine::destroySingleton();
|
|
}
|
|
|
|
UTEST( UIHTML, TextureReplaceInvalidatesRichTextAncestors ) {
|
|
auto win = Engine::instance()->createWindow(
|
|
WindowSettings( 1024, 768, "texture replace img relayout", WindowStyle::Default,
|
|
WindowBackend::Default, 32, {}, 1, false, true ),
|
|
ContextSettings( false, 0, 0, GLv_default, true, false ) );
|
|
|
|
UISceneNode* sceneNode = init_test_inline_block();
|
|
|
|
UIWebView* webView = UIWebView::New();
|
|
webView->setParent( sceneNode->getRoot() );
|
|
webView->setPixelsSize( win->getWidth(), win->getHeight() );
|
|
webView->setLayoutSizePolicy( SizePolicy::Fixed, SizePolicy::Fixed );
|
|
UISceneNode* documentScene = webView->getDocumentSceneNode();
|
|
ASSERT_TRUE( documentScene != nullptr );
|
|
std::string html = R"html(
|
|
<!doctype html>
|
|
<html>
|
|
<body>
|
|
<article id="article" style="display:block; width: 480px;">
|
|
<p>Before image</p>
|
|
<img id="late-img">
|
|
<p>After image</p>
|
|
</article>
|
|
</body>
|
|
</html>
|
|
)html";
|
|
documentScene->setURI( "file://delayed-image-resize.html" );
|
|
documentScene->loadLayoutFromString( HTMLFormatter::HTMLtoXML( html ),
|
|
webView->getDocumentContainer(),
|
|
String::hash( "delayed-image-resize" ) );
|
|
|
|
win->getInput()->update();
|
|
SceneManager::instance()->update();
|
|
documentScene->updateDirtyLayouts();
|
|
|
|
auto* articleNode = documentScene->getRoot()->find( "article" );
|
|
auto* bodyNode = documentScene->getRoot()->findByType( UI_TYPE_HTML_BODY );
|
|
auto* article = articleNode ? articleNode->asType<UIRichText>() : nullptr;
|
|
auto* body = bodyNode ? bodyNode->asType<UIWidget>() : nullptr;
|
|
auto* doc = webView->getDocumentContainer();
|
|
auto images = documentScene->getRoot()->findAllByTag( "img" );
|
|
ASSERT_TRUE( article != nullptr );
|
|
ASSERT_TRUE( body != nullptr );
|
|
ASSERT_TRUE( doc != nullptr );
|
|
ASSERT_EQ( images.size(), (size_t)1 );
|
|
auto* img = images[0]->asType<UIHTMLImage>();
|
|
ASSERT_TRUE( img != nullptr );
|
|
|
|
TexturePtr texture =
|
|
TextureFactory::instance()->createEmptyTexture( 1, 1, 4, Color::Transparent );
|
|
ASSERT_TRUE( texture != nullptr );
|
|
img->setDrawable( texture );
|
|
documentScene->updateDirtyLayouts();
|
|
|
|
Float articleInitialHeight = article->getPixelsSize().getHeight();
|
|
Float bodyInitialHeight = body->getPixelsSize().getHeight();
|
|
Float docInitialHeight = doc->getPixelsSize().getHeight();
|
|
|
|
Image loadedImage( 320, 180, 4, Color::White );
|
|
texture->replace( &loadedImage );
|
|
SceneManager::instance()->update();
|
|
documentScene->updateDirtyLayouts();
|
|
|
|
EXPECT_GT( article->getPixelsSize().getHeight(), articleInitialHeight + 150.f );
|
|
EXPECT_GT( body->getPixelsSize().getHeight(), bodyInitialHeight + 150.f );
|
|
EXPECT_GT( doc->getPixelsSize().getHeight(), docInitialHeight + 150.f );
|
|
|
|
texture.reset();
|
|
Engine::destroySingleton();
|
|
}
|
|
|
|
UTEST( UIHTML, DeferredFileImageLoadsAsync ) {
|
|
auto win = Engine::instance()->createWindow(
|
|
WindowSettings( 1024, 768, "deferred file img async", WindowStyle::Default,
|
|
WindowBackend::Default, 32, {}, 1, false, true ),
|
|
ContextSettings( false, 0, 0, GLv_default, true, false ) );
|
|
const std::string processPath = Sys::getProcessPath();
|
|
const std::string imagePath = processPath + "../assets/icon/ee.png";
|
|
ASSERT_TRUE( FileSystem::fileExists( imagePath ) );
|
|
|
|
auto threadPool = ThreadPool::createShared( 1 );
|
|
std::atomic<bool> releaseBlocker{ false };
|
|
std::atomic<bool> blockerStarted{ false };
|
|
threadPool->run( [&] {
|
|
blockerStarted.store( true, std::memory_order_release );
|
|
while ( !releaseBlocker.load( std::memory_order_acquire ) )
|
|
Sys::sleep( Milliseconds( 1 ) );
|
|
} );
|
|
|
|
for ( int i = 0; i < 100 && !blockerStarted.load( std::memory_order_acquire ); ++i )
|
|
Sys::sleep( Milliseconds( 1 ) );
|
|
if ( !blockerStarted.load( std::memory_order_acquire ) )
|
|
releaseBlocker.store( true, std::memory_order_release );
|
|
ASSERT_TRUE( blockerStarted.load( std::memory_order_acquire ) );
|
|
|
|
UISceneNode* sceneNode = init_test_inline_block();
|
|
sceneNode->setThreadPool( threadPool );
|
|
sceneNode->setURI( URI( "file://" + processPath ) );
|
|
|
|
std::string html = R"html(
|
|
<!doctype html>
|
|
<html>
|
|
<body>
|
|
<img id="deferred-img" defer src="../assets/icon/ee.png">
|
|
</body>
|
|
</html>
|
|
)html";
|
|
sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( html ), sceneNode->getRoot(),
|
|
String::hash( "deferred-file-image" ) );
|
|
|
|
win->getInput()->update();
|
|
SceneManager::instance()->update();
|
|
sceneNode->updateDirtyLayouts();
|
|
|
|
auto* imgNode = sceneNode->getRoot()->find( "deferred-img" );
|
|
if ( imgNode == nullptr )
|
|
releaseBlocker.store( true, std::memory_order_release );
|
|
ASSERT_TRUE( imgNode != nullptr );
|
|
auto* img = imgNode->asType<UIHTMLImage>();
|
|
if ( img == nullptr )
|
|
releaseBlocker.store( true, std::memory_order_release );
|
|
ASSERT_TRUE( img != nullptr );
|
|
EXPECT_TRUE( img->getDrawable() == nullptr );
|
|
|
|
releaseBlocker.store( true, std::memory_order_release );
|
|
|
|
for ( int i = 0; i < 200 && img->getDrawable() == nullptr; ++i ) {
|
|
win->getInput()->update();
|
|
SceneManager::instance()->update();
|
|
sceneNode->updateDirtyLayouts();
|
|
Sys::sleep( Milliseconds( 1 ) );
|
|
}
|
|
|
|
ASSERT_TRUE( img->getDrawable() != nullptr );
|
|
EXPECT_GT( img->getPixelsSize().getWidth(), 0.f );
|
|
EXPECT_GT( img->getPixelsSize().getHeight(), 0.f );
|
|
|
|
Engine::destroySingleton();
|
|
}
|
|
|
|
UTEST( UIHTML, DeferredFileImageReusesCachedTexture ) {
|
|
auto win = Engine::instance()->createWindow(
|
|
WindowSettings( 1024, 768, "deferred file img cache reuse", WindowStyle::Default,
|
|
WindowBackend::Default, 32, {}, 1, false, true ),
|
|
ContextSettings( false, 0, 0, GLv_default, true, false ) );
|
|
const std::string processPath = Sys::getProcessPath();
|
|
|
|
UISceneNode* sceneNode = init_test_inline_block();
|
|
sceneNode->setThreadPool( ThreadPool::createShared( 1 ) );
|
|
sceneNode->setURI( URI( "file://" + processPath ) );
|
|
URI imageURI = sceneNode->solveRelativePath( URI( "../assets/icon/ee.png" ) );
|
|
ASSERT_TRUE( FileSystem::fileExists( imageURI.getFSPath() ) );
|
|
DrawablePtr cached = sceneNode->getDrawableResolver().resolve( imageURI.toString() );
|
|
ASSERT_TRUE( cached != nullptr );
|
|
|
|
sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( R"html(
|
|
<!doctype html>
|
|
<html>
|
|
<body>
|
|
<img id="first-img" defer src="../assets/icon/ee.png">
|
|
<img id="second-img" defer src="../assets/icon/ee.png">
|
|
</body>
|
|
</html>
|
|
)html" ),
|
|
sceneNode->getRoot(), String::hash( "deferred-file-cache" ) );
|
|
|
|
win->getInput()->update();
|
|
SceneManager::instance()->update();
|
|
sceneNode->updateDirtyLayouts();
|
|
|
|
auto* firstNode = sceneNode->getRoot()->find( "first-img" );
|
|
auto* secondNode = sceneNode->getRoot()->find( "second-img" );
|
|
ASSERT_TRUE( firstNode != nullptr );
|
|
ASSERT_TRUE( secondNode != nullptr );
|
|
auto* first = firstNode->asType<UIHTMLImage>();
|
|
auto* second = secondNode->asType<UIHTMLImage>();
|
|
ASSERT_TRUE( first != nullptr );
|
|
ASSERT_TRUE( second != nullptr );
|
|
EXPECT_EQ( getDrawableTexture( cached ), getDrawableTexture( first->getDrawable() ) );
|
|
EXPECT_EQ( getDrawableTexture( cached ), getDrawableTexture( second->getDrawable() ) );
|
|
EXPECT_NE( first->getDrawable().get(), second->getDrawable().get() );
|
|
|
|
cached.reset();
|
|
Engine::destroySingleton();
|
|
}
|
|
|
|
UTEST( UIHTML, RemoteImageReusesCachedTexture ) {
|
|
auto win = Engine::instance()->createWindow(
|
|
WindowSettings( 1024, 768, "remote img cache reuse", WindowStyle::Default,
|
|
WindowBackend::Default, 32, {}, 1, false, true ),
|
|
ContextSettings( false, 0, 0, GLv_default, true, false ) );
|
|
const std::string imageURL = "http://127.0.0.1:1/eepp-cached-image.png";
|
|
|
|
UISceneNode* sceneNode = init_test_inline_block();
|
|
TexturePtr cached = TextureFactory::instance()->createEmptyTexture(
|
|
8, 8, 4, Color::White, false, Texture::ClampMode::ClampToEdge, false, false, imageURL );
|
|
ASSERT_TRUE( cached != nullptr );
|
|
sceneNode->getResourceScope()->publishLocal( imageURL, cached );
|
|
|
|
sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( R"html(
|
|
<!doctype html>
|
|
<html>
|
|
<body>
|
|
<img id="first-img" src="http://127.0.0.1:1/eepp-cached-image.png">
|
|
<img id="second-img" src="http://127.0.0.1:1/eepp-cached-image.png">
|
|
</body>
|
|
</html>
|
|
)html" ),
|
|
sceneNode->getRoot(), String::hash( "remote-image-cache" ) );
|
|
|
|
win->getInput()->update();
|
|
SceneManager::instance()->update();
|
|
sceneNode->updateDirtyLayouts();
|
|
|
|
auto* firstNode = sceneNode->getRoot()->find( "first-img" );
|
|
auto* secondNode = sceneNode->getRoot()->find( "second-img" );
|
|
ASSERT_TRUE( firstNode != nullptr );
|
|
ASSERT_TRUE( secondNode != nullptr );
|
|
auto* first = firstNode->asType<UIHTMLImage>();
|
|
auto* second = secondNode->asType<UIHTMLImage>();
|
|
ASSERT_TRUE( first != nullptr );
|
|
ASSERT_TRUE( second != nullptr );
|
|
EXPECT_EQ( cached.get(), getDrawableTexture( first->getDrawable() ) );
|
|
EXPECT_EQ( cached.get(), getDrawableTexture( second->getDrawable() ) );
|
|
EXPECT_NE( first->getDrawable().get(), second->getDrawable().get() );
|
|
|
|
cached.reset();
|
|
Engine::destroySingleton();
|
|
}
|
|
|
|
UTEST( UIHTML, DeferredFileImageRelayoutsAncestors ) {
|
|
auto win = Engine::instance()->createWindow(
|
|
WindowSettings( 1024, 768, "deferred file img relayout", WindowStyle::Default,
|
|
WindowBackend::Default, 32, {}, 1, false, true ),
|
|
ContextSettings( false, 0, 0, GLv_default, true, false ) );
|
|
|
|
const std::string imageName = "eepp_deferred_file_image_relayout.png";
|
|
const std::string imagePath = Sys::getTempPath() + imageName;
|
|
Image testImage( 320, 180, 4, Color::White );
|
|
ASSERT_TRUE( testImage.saveToFile( imagePath, Image::SaveType::PNG ) );
|
|
|
|
UISceneNode* sceneNode = init_test_inline_block();
|
|
UIWebView* webView = UIWebView::New();
|
|
webView->setParent( sceneNode->getRoot() );
|
|
webView->setPixelsSize( win->getWidth(), win->getHeight() );
|
|
webView->setLayoutSizePolicy( SizePolicy::Fixed, SizePolicy::Fixed );
|
|
|
|
UISceneNode* documentScene = webView->getDocumentSceneNode();
|
|
ASSERT_TRUE( documentScene != nullptr );
|
|
|
|
auto threadPool = ThreadPool::createShared( 1 );
|
|
std::atomic<bool> releaseBlocker{ false };
|
|
std::atomic<bool> blockerStarted{ false };
|
|
threadPool->run( [&] {
|
|
blockerStarted.store( true, std::memory_order_release );
|
|
while ( !releaseBlocker.load( std::memory_order_acquire ) )
|
|
Sys::sleep( Milliseconds( 1 ) );
|
|
} );
|
|
|
|
for ( int i = 0; i < 100 && !blockerStarted.load( std::memory_order_acquire ); ++i )
|
|
Sys::sleep( Milliseconds( 1 ) );
|
|
if ( !blockerStarted.load( std::memory_order_acquire ) )
|
|
releaseBlocker.store( true, std::memory_order_release );
|
|
ASSERT_TRUE( blockerStarted.load( std::memory_order_acquire ) );
|
|
|
|
documentScene->setThreadPool( threadPool );
|
|
documentScene->setURI( URI( "file://" + Sys::getTempPath() ) );
|
|
|
|
std::string html = R"html(
|
|
<!doctype html>
|
|
<html>
|
|
<body>
|
|
<article id="article" style="display:block; width: 480px;">
|
|
<p>Before image</p>
|
|
<img id="async-img" defer src=")html" +
|
|
imageName +
|
|
R"html(">
|
|
<p id="after-img">After image</p>
|
|
</article>
|
|
</body>
|
|
</html>
|
|
)html";
|
|
documentScene->loadLayoutFromString( HTMLFormatter::HTMLtoXML( html ),
|
|
webView->getDocumentContainer(),
|
|
String::hash( "deferred-file-image-relayout" ) );
|
|
|
|
win->getInput()->update();
|
|
SceneManager::instance()->update();
|
|
documentScene->updateDirtyLayouts();
|
|
|
|
auto* article = documentScene->getRoot()->find( "article" )->asType<UIRichText>();
|
|
auto* body = documentScene->getRoot()->findByType( UI_TYPE_HTML_BODY )->asType<UIWidget>();
|
|
auto* doc = webView->getDocumentContainer();
|
|
auto* imgNode = documentScene->getRoot()->find( "async-img" );
|
|
if ( article == nullptr || body == nullptr || doc == nullptr || imgNode == nullptr )
|
|
releaseBlocker.store( true, std::memory_order_release );
|
|
ASSERT_TRUE( article != nullptr );
|
|
ASSERT_TRUE( body != nullptr );
|
|
ASSERT_TRUE( doc != nullptr );
|
|
ASSERT_TRUE( imgNode != nullptr );
|
|
auto* img = imgNode->asType<UIHTMLImage>();
|
|
if ( img == nullptr )
|
|
releaseBlocker.store( true, std::memory_order_release );
|
|
ASSERT_TRUE( img != nullptr );
|
|
EXPECT_TRUE( img->getDrawable() == nullptr );
|
|
|
|
Float articleInitialHeight = article->getPixelsSize().getHeight();
|
|
Float bodyInitialHeight = body->getPixelsSize().getHeight();
|
|
Float docInitialHeight = doc->getPixelsSize().getHeight();
|
|
|
|
releaseBlocker.store( true, std::memory_order_release );
|
|
|
|
for ( int i = 0; i < 200 && article->getPixelsSize().getHeight() < articleInitialHeight + 150.f;
|
|
++i ) {
|
|
win->getInput()->update();
|
|
SceneManager::instance()->update();
|
|
documentScene->updateDirtyLayouts();
|
|
Sys::sleep( Milliseconds( 1 ) );
|
|
}
|
|
|
|
ASSERT_TRUE( img->getDrawable() != nullptr );
|
|
EXPECT_GT( article->getPixelsSize().getHeight(), articleInitialHeight + 150.f );
|
|
EXPECT_GT( body->getPixelsSize().getHeight(), bodyInitialHeight + 150.f );
|
|
EXPECT_GT( doc->getPixelsSize().getHeight(), docInitialHeight + 150.f );
|
|
|
|
FileSystem::fileRemove( imagePath );
|
|
Engine::destroySingleton();
|
|
}
|
|
|
|
UTEST( UIHTML, RonStonerDeferredImagesUpdateDocumentHeight ) {
|
|
auto threadPool = ThreadPool::createShared( eemax<int>( 4, Sys::getCPUCount() ) );
|
|
auto win = Engine::instance()->createWindow(
|
|
WindowSettings( 1280, 720, "ron stoner deferred image relayout", WindowStyle::Default,
|
|
WindowBackend::Default, 32, {}, 1, false, true ),
|
|
ContextSettings( false, 0, 0, GLv_default, true, false ) );
|
|
FileSystem::changeWorkingDirectory( Sys::getProcessPath() );
|
|
|
|
FontTrueType* font = FontTrueType::New( "NotoSans-Regular" ).get();
|
|
font->loadFromFile( "../assets/fonts/NotoSans-Regular.ttf" );
|
|
ASSERT_TRUE( font != nullptr && font->loaded() );
|
|
FontFamily::loadFromRegular( font );
|
|
|
|
UISceneNode* sceneNode = UISceneNode::New();
|
|
SceneManager::instance()->add( sceneNode );
|
|
SceneManager::instance()->setCurrentUISceneNode( sceneNode );
|
|
sceneNode->setThreadPool( threadPool );
|
|
sceneNode->getUIThemeManager()->setDefaultFont( font );
|
|
|
|
UIWebView* webView = UIWebView::New();
|
|
webView->setParent( sceneNode->getRoot() );
|
|
webView->setPixelsSize( win->getWidth(), win->getHeight() );
|
|
webView->setLayoutSizePolicy( SizePolicy::Fixed, SizePolicy::Fixed );
|
|
|
|
const std::string path = Sys::getProcessPath() + "assets/html/ron_stoner.html";
|
|
ASSERT_TRUE( FileSystem::fileExists( path ) );
|
|
webView->loadURI( URI( "file://" + path ) );
|
|
|
|
auto pump = [&] {
|
|
win->getInput()->update();
|
|
SceneManager::instance()->update( Seconds( 1.f / 60.f ) );
|
|
if ( auto* documentScene = webView->getDocumentSceneNode() )
|
|
documentScene->updateDirtyLayouts();
|
|
Sys::sleep( Milliseconds( 1 ) );
|
|
};
|
|
|
|
UISceneNode* documentScene = webView->getDocumentSceneNode();
|
|
ASSERT_TRUE( documentScene != nullptr );
|
|
|
|
UIWidget* body = nullptr;
|
|
WidgetQueryResult images;
|
|
for ( int i = 0; i < 300; ++i ) {
|
|
pump();
|
|
body = documentScene->getRoot()->findByType( UI_TYPE_HTML_BODY )->asType<UIWidget>();
|
|
images = documentScene->getRoot()->findAllByTag( "img" );
|
|
if ( body != nullptr && !images.empty() )
|
|
break;
|
|
}
|
|
|
|
ASSERT_TRUE( body != nullptr );
|
|
ASSERT_FALSE( images.empty() );
|
|
|
|
auto allImagesLoaded = [&images] {
|
|
for ( auto* image : images ) {
|
|
auto* uiImage = image ? image->asType<UIHTMLImage>() : nullptr;
|
|
if ( uiImage == nullptr || uiImage->getDrawable() == nullptr )
|
|
return false;
|
|
}
|
|
return true;
|
|
};
|
|
|
|
for ( int i = 0; i < 600 && !allImagesLoaded(); ++i )
|
|
pump();
|
|
|
|
ASSERT_TRUE( allImagesLoaded() );
|
|
|
|
for ( int i = 0; i < 30; ++i )
|
|
pump();
|
|
|
|
ASSERT_GT( images.size(), 1u );
|
|
UIWidget* champion = images[1];
|
|
UIWidget* championParagraph = champion->getParent()->asType<UIWidget>();
|
|
ASSERT_TRUE( championParagraph != nullptr );
|
|
EXPECT_NEAR( champion->getPixelsSize().getWidth(),
|
|
championParagraph->getPixelsSize().getWidth(), 1.f );
|
|
EXPECT_GT( champion->getPixelsSize().getHeight(), 500.f );
|
|
EXPECT_GE( championParagraph->getPixelsSize().getHeight(),
|
|
champion->getPixelsSize().getHeight() );
|
|
Node* following = championParagraph->getNextNode();
|
|
while ( following &&
|
|
( !following->isWidget() || following->asType<UIWidget>()->getElementTag() != "p" ) )
|
|
following = following->getNextNode();
|
|
ASSERT_TRUE( following != nullptr );
|
|
EXPECT_GE( following->asType<UIWidget>()->getPixelsPosition().y + 1.f,
|
|
championParagraph->getPixelsPosition().y +
|
|
championParagraph->getPixelsSize().getHeight() );
|
|
|
|
const Float bodyHeightAfterAsyncLoad = body->getPixelsSize().getHeight();
|
|
const Float docHeightAfterAsyncLoad =
|
|
webView->getDocumentContainer()->getPixelsSize().getHeight();
|
|
ASSERT_GT( bodyHeightAfterAsyncLoad, 1000.f );
|
|
ASSERT_GT( docHeightAfterAsyncLoad, 1000.f );
|
|
|
|
webView->setPixelsSize( 1180, 680 );
|
|
for ( int i = 0; i < 30; ++i )
|
|
pump();
|
|
webView->setPixelsSize( win->getWidth(), win->getHeight() );
|
|
for ( int i = 0; i < 30; ++i )
|
|
pump();
|
|
|
|
const Float bodyHeightAfterResize = body->getPixelsSize().getHeight();
|
|
const Float docHeightAfterResize = webView->getDocumentContainer()->getPixelsSize().getHeight();
|
|
|
|
EXPECT_GE( bodyHeightAfterAsyncLoad + 1.f, bodyHeightAfterResize );
|
|
EXPECT_GE( docHeightAfterAsyncLoad + 1.f, docHeightAfterResize );
|
|
|
|
Engine::destroySingleton();
|
|
}
|
|
|
|
UTEST( UIHTML, NewsBlurStoryArchiveLayoutStabilizes ) {
|
|
auto win = Engine::instance()->createWindow(
|
|
WindowSettings( 1280, 720, "newsblur story archive layout stability", WindowStyle::Default,
|
|
WindowBackend::Default, 32, {}, 1, false, true ),
|
|
ContextSettings( false, 0, 0, GLv_default, true, false ) );
|
|
FileSystem::changeWorkingDirectory( Sys::getProcessPath() );
|
|
|
|
FontTrueType* font = FontTrueType::New( "NotoSans-Regular" ).get();
|
|
font->loadFromFile( "../assets/fonts/NotoSans-Regular.ttf" );
|
|
ASSERT_TRUE( font != nullptr && font->loaded() );
|
|
FontFamily::loadFromRegular( font );
|
|
|
|
UISceneNode* sceneNode = UISceneNode::New();
|
|
SceneManager::instance()->add( sceneNode );
|
|
SceneManager::instance()->setCurrentUISceneNode( sceneNode );
|
|
sceneNode->getUIThemeManager()->setDefaultFont( font );
|
|
|
|
UIWebView* webView = UIWebView::New();
|
|
webView->setParent( sceneNode->getRoot() );
|
|
webView->setPixelsSize( win->getWidth(), win->getHeight() );
|
|
webView->setLayoutSizePolicy( SizePolicy::Fixed, SizePolicy::Fixed );
|
|
|
|
const std::string path = Sys::getProcessPath() + "assets/html/story_archive_newsblur.html";
|
|
ASSERT_TRUE( FileSystem::fileExists( path ) );
|
|
webView->loadURI( URI( "file://" + path ) );
|
|
|
|
auto pump = [&] {
|
|
win->getInput()->update();
|
|
SceneManager::instance()->update( Seconds( 1.f / 60.f ) );
|
|
if ( auto* documentScene = webView->getDocumentSceneNode() )
|
|
documentScene->updateDirtyLayouts();
|
|
Sys::sleep( Milliseconds( 1 ) );
|
|
};
|
|
|
|
UISceneNode* documentScene = webView->getDocumentSceneNode();
|
|
ASSERT_TRUE( documentScene != nullptr );
|
|
|
|
UIWidget* body = nullptr;
|
|
for ( int i = 0; i < 300; ++i ) {
|
|
pump();
|
|
auto* bodyNode = documentScene->getRoot()->findByType( UI_TYPE_HTML_BODY );
|
|
body = bodyNode ? bodyNode->asType<UIWidget>() : nullptr;
|
|
if ( body != nullptr && body->getPixelsSize().getHeight() > 0 )
|
|
break;
|
|
}
|
|
|
|
ASSERT_TRUE( body != nullptr );
|
|
|
|
bool stabilized = false;
|
|
Uint64 lastInvalidations = 0;
|
|
Uint64 lastTreeUpdates = 0;
|
|
Uint64 lastRichTextRebuilds = 0;
|
|
int stableFrames = 0;
|
|
for ( int i = 0; i < 240; ++i ) {
|
|
UILayout::resetMetrics();
|
|
pump();
|
|
auto metrics = UILayout::getMetrics();
|
|
lastInvalidations = metrics.invalidations;
|
|
lastTreeUpdates = metrics.treeUpdates;
|
|
lastRichTextRebuilds = metrics.richTextRebuilds;
|
|
|
|
if ( metrics.invalidations == 0 && metrics.treeUpdates == 0 &&
|
|
metrics.richTextRebuilds == 0 ) {
|
|
if ( ++stableFrames >= 10 ) {
|
|
stabilized = true;
|
|
break;
|
|
}
|
|
} else {
|
|
stableFrames = 0;
|
|
}
|
|
}
|
|
UILayout::setMetricsEnabled( false );
|
|
|
|
if ( !stabilized ) {
|
|
std::cout << "NewsBlur layout did not stabilize: lastInvalidations=" << lastInvalidations
|
|
<< " lastTreeUpdates=" << lastTreeUpdates
|
|
<< " lastRichTextRebuilds=" << lastRichTextRebuilds << std::endl;
|
|
}
|
|
EXPECT_TRUE( stabilized );
|
|
|
|
Engine::destroySingleton();
|
|
}
|
|
|
|
UTEST( UIHTML, HtmlContainsTableBodyHeight ) {
|
|
auto win = Engine::instance()->createWindow(
|
|
WindowSettings( 1024, 768, "html contains table body height", WindowStyle::Default,
|
|
WindowBackend::Default, 32, {}, 1, false, true ),
|
|
ContextSettings( false, 0, 0, GLv_default, true, false ) );
|
|
|
|
UISceneNode* sceneNode = init_test_inline_block();
|
|
|
|
UIWebView* webView = UIWebView::New();
|
|
webView->setParent( sceneNode->getRoot() );
|
|
webView->setPixelsSize( win->getWidth(), win->getHeight() );
|
|
webView->setLayoutSizePolicy( SizePolicy::Fixed, SizePolicy::Fixed );
|
|
UISceneNode* documentScene = webView->getDocumentSceneNode();
|
|
ASSERT_TRUE( documentScene != nullptr );
|
|
|
|
FileSystem::changeWorkingDirectory( Sys::getProcessPath() );
|
|
std::string html;
|
|
ASSERT_TRUE( FileSystem::fileGet( "assets/html/hn_frontpage.html", html ) );
|
|
for ( std::string::size_type pos = 0;
|
|
( pos = html.find( "<link rel=\"stylesheet\"", pos ) ) != std::string::npos; ) {
|
|
auto end = html.find( "/>", pos );
|
|
ASSERT_TRUE( end != std::string::npos );
|
|
html.erase( pos, end + 2 - pos );
|
|
}
|
|
|
|
documentScene->setURI( "file://html-table-body-height.html" );
|
|
documentScene->loadLayoutFromString( HTMLFormatter::HTMLtoXML( html ),
|
|
webView->getDocumentContainer(),
|
|
String::hash( "html-table-body-height" ) );
|
|
|
|
win->getInput()->update();
|
|
SceneManager::instance()->update();
|
|
documentScene->updateDirtyLayouts();
|
|
|
|
std::string css;
|
|
ASSERT_TRUE( FileSystem::fileGet( "assets/html/base.css", css ) );
|
|
css += "\n";
|
|
std::string newsCss;
|
|
ASSERT_TRUE( FileSystem::fileGet( "assets/html/news.css", newsCss ) );
|
|
css += newsCss;
|
|
documentScene->runOnMainThread( [documentScene, css = std::move( css )] {
|
|
documentScene->combineStyleSheet( css, true,
|
|
String::hash( "html-table-body-height-late-css" ) );
|
|
} );
|
|
SceneManager::instance()->update();
|
|
documentScene->updateDirtyLayouts();
|
|
SceneManager::instance()->update();
|
|
documentScene->updateDirtyLayouts();
|
|
|
|
auto* htmlNode = documentScene->getRoot()->findByType( UI_TYPE_HTML_HTML )->asType<UIWidget>();
|
|
auto* body = documentScene->getRoot()->findByType( UI_TYPE_HTML_BODY )->asType<UIWidget>();
|
|
auto* table = documentScene->getRoot()->find( "hnmain" )->asType<UIWidget>();
|
|
ASSERT_TRUE( htmlNode != nullptr );
|
|
ASSERT_TRUE( body != nullptr );
|
|
ASSERT_TRUE( table != nullptr );
|
|
|
|
EXPECT_GE( htmlNode->getPixelsSize().getHeight(), body->getPixelsSize().getHeight() );
|
|
EXPECT_GE( body->getPixelsSize().getHeight() + 4.f, table->getPixelsSize().getHeight() );
|
|
EXPECT_GT( table->getPixelsSize().getHeight(), 500.f );
|
|
|
|
Engine::destroySingleton();
|
|
}
|
|
|
|
UTEST( UIHTML, BodyDocumentContentMinHeightCanShrink ) {
|
|
auto win = Engine::instance()->createWindow(
|
|
WindowSettings( 800, 600, "body content min height shrinks", WindowStyle::Default,
|
|
WindowBackend::Default, 32, {}, 1, false, true ),
|
|
ContextSettings( false, 0, 0, GLv_default, true, false ) );
|
|
|
|
UISceneNode* sceneNode = init_test_inline_block();
|
|
|
|
UIWebView* webView = UIWebView::New();
|
|
webView->setParent( sceneNode->getRoot() );
|
|
webView->setPixelsSize( win->getWidth(), win->getHeight() );
|
|
webView->setLayoutSizePolicy( SizePolicy::Fixed, SizePolicy::Fixed );
|
|
UISceneNode* documentScene = webView->getDocumentSceneNode();
|
|
ASSERT_TRUE( documentScene != nullptr );
|
|
|
|
std::string html = R"html(
|
|
<!doctype html>
|
|
<html>
|
|
<body style="margin: 0;">
|
|
<div id="spacer" style="display: block; width: 100px; height: 900px;"></div>
|
|
</body>
|
|
</html>
|
|
)html";
|
|
|
|
documentScene->setURI( "file://body-content-min-height-shrink.html" );
|
|
documentScene->loadLayoutFromString( HTMLFormatter::HTMLtoXML( html ),
|
|
webView->getDocumentContainer(),
|
|
String::hash( "body-content-min-height-shrink" ) );
|
|
webView->refreshDocumentLayout();
|
|
|
|
win->getInput()->update();
|
|
SceneManager::instance()->update();
|
|
documentScene->updateDirtyLayouts();
|
|
|
|
auto* htmlNode = documentScene->getRoot()->findByType( UI_TYPE_HTML_HTML )->asType<UIWidget>();
|
|
auto* body = documentScene->getRoot()->findByType( UI_TYPE_HTML_BODY )->asType<UIWidget>();
|
|
auto* spacer = documentScene->getRoot()->find( "spacer" )->asType<UIWidget>();
|
|
ASSERT_TRUE( htmlNode != nullptr );
|
|
ASSERT_TRUE( body != nullptr );
|
|
ASSERT_TRUE( spacer != nullptr );
|
|
|
|
const Float tallBodyHeight = body->getPixelsSize().getHeight();
|
|
EXPECT_GT( spacer->getPixelsSize().getHeight(), 850.f );
|
|
EXPECT_GT( tallBodyHeight, 850.f );
|
|
|
|
spacer->setStyleSheetProperty( StyleSheetProperty( "height", "120px" ) );
|
|
win->getInput()->update();
|
|
SceneManager::instance()->update();
|
|
documentScene->updateDirtyLayouts();
|
|
win->getInput()->update();
|
|
SceneManager::instance()->update();
|
|
documentScene->updateDirtyLayouts();
|
|
|
|
EXPECT_LT( spacer->getPixelsSize().getHeight(), 180.f );
|
|
EXPECT_LT( body->getPixelsSize().getHeight(), tallBodyHeight - 250.f );
|
|
EXPECT_LT( htmlNode->getPixelsSize().getHeight(), tallBodyHeight - 250.f );
|
|
EXPECT_GE( body->getPixelsSize().getHeight() + 1.f, webView->getPixelsSize().getHeight() );
|
|
EXPECT_GE( htmlNode->getPixelsSize().getHeight() + 1.f, webView->getPixelsSize().getHeight() );
|
|
|
|
Engine::destroySingleton();
|
|
}
|
|
|
|
UTEST( UIHTML, BodyNoOpContentHeightChangeDoesNotDirtyHtml ) {
|
|
auto win = Engine::instance()->createWindow(
|
|
WindowSettings( 800, 600, "body no-op content height change", WindowStyle::Default,
|
|
WindowBackend::Default, 32, {}, 1, false, true ),
|
|
ContextSettings( false, 0, 0, GLv_default, true, false ) );
|
|
|
|
UISceneNode* sceneNode = init_test_inline_block();
|
|
|
|
UIWebView* webView = UIWebView::New();
|
|
webView->setParent( sceneNode->getRoot() );
|
|
webView->setPixelsSize( win->getWidth(), win->getHeight() );
|
|
webView->setLayoutSizePolicy( SizePolicy::Fixed, SizePolicy::Fixed );
|
|
|
|
std::string html = R"html(
|
|
<!doctype html>
|
|
<html>
|
|
<body style="margin: 0;">
|
|
<div id="spacer" style="display: block; width: 100px; height: 900px;"></div>
|
|
</body>
|
|
</html>
|
|
)html";
|
|
|
|
UISceneNode* documentScene = webView->getDocumentSceneNode();
|
|
ASSERT_TRUE( documentScene != nullptr );
|
|
documentScene->setURI( "file://body-no-op-content-height-change.html" );
|
|
documentScene->loadLayoutFromString( HTMLFormatter::HTMLtoXML( html ),
|
|
webView->getDocumentContainer(),
|
|
String::hash( "body-no-op-content-height-change" ) );
|
|
webView->refreshDocumentLayout();
|
|
|
|
win->getInput()->update();
|
|
SceneManager::instance()->update();
|
|
documentScene->updateDirtyLayouts();
|
|
win->getInput()->update();
|
|
SceneManager::instance()->update();
|
|
documentScene->updateDirtyLayouts();
|
|
|
|
auto* htmlNode = documentScene->getRoot()->findByType( UI_TYPE_HTML_HTML )->asType<UILayout>();
|
|
auto* body = documentScene->getRoot()->findByType( UI_TYPE_HTML_BODY )->asType<UIWidget>();
|
|
auto* spacer = documentScene->getRoot()->find( "spacer" )->asType<UIWidget>();
|
|
ASSERT_TRUE( htmlNode != nullptr );
|
|
ASSERT_TRUE( body != nullptr );
|
|
ASSERT_TRUE( spacer != nullptr );
|
|
ASSERT_GT( body->getPixelsSize().getHeight(), 850.f );
|
|
ASSERT_FALSE( htmlNode->isLayoutDirty() );
|
|
|
|
spacer->setPixelsSize(
|
|
{ spacer->getPixelsSize().getWidth() + 25.f, spacer->getPixelsSize().getHeight() } );
|
|
EXPECT_FALSE( htmlNode->isLayoutDirty() );
|
|
win->getInput()->update();
|
|
SceneManager::instance()->update();
|
|
documentScene->updateDirtyLayouts();
|
|
|
|
UILayout::resetMetrics();
|
|
for ( int i = 0; i < 4; ++i ) {
|
|
win->getInput()->update();
|
|
SceneManager::instance()->update();
|
|
documentScene->updateDirtyLayouts();
|
|
}
|
|
auto metrics = UILayout::getMetrics();
|
|
UILayout::setMetricsEnabled( false );
|
|
EXPECT_EQ( 0u, metrics.invalidations );
|
|
EXPECT_EQ( 0u, metrics.treeUpdates );
|
|
EXPECT_EQ( 0u, metrics.richTextRebuilds );
|
|
|
|
Engine::destroySingleton();
|
|
}
|
|
|
|
UTEST( UIHTML, DeferredCSSKeepsTableHeightStableAfterViewportResize ) {
|
|
std::shared_ptr<ThreadPool> threadPool(
|
|
ThreadPool::createShared( eemax<int>( 4, Sys::getCPUCount() ) ) );
|
|
Http::setThreadPool( threadPool );
|
|
SystemFontResolver::setEnabled( true );
|
|
UIApplication app(
|
|
WindowSettings{ 1280, 720, "deferred css table height stable", WindowStyle::Default,
|
|
WindowBackend::Default, 32 },
|
|
UIApplication::Settings( Sys::getProcessPath() + ".." + FileSystem::getOSSlash(), 1.5 ),
|
|
ContextSettings( false, ContextSettings::FrameRateLimitScreenRefreshRate ) );
|
|
|
|
FileSystem::changeWorkingDirectory( Sys::getProcessPath() );
|
|
|
|
auto win = app.getWindow();
|
|
ASSERT_TRUE( win != nullptr );
|
|
|
|
win->setPosition( 0, 0 );
|
|
|
|
UISceneNode* ui = app.getUI();
|
|
ASSERT_TRUE( ui != nullptr );
|
|
ui->setThreadPool( threadPool );
|
|
ui->setColorSchemePreference( ColorSchemeExtPreference::Light );
|
|
|
|
ui->setURI( "file://" + Sys::getProcessPath() + "assets/html/" );
|
|
ui->loadLayoutFromString( R"xml(
|
|
<vbox layout_width="match_parent" layout_height="match_parent">
|
|
<WebView id="webview" layout_width="match_parent" layout_height="0" layout_weight="1" />
|
|
</vbox>
|
|
)xml",
|
|
nullptr, app.getStyleSheetDefaultMarker() );
|
|
|
|
UIWebView* webView = ui->find( "webview" )->asType<UIWebView>();
|
|
webView->setStyleSheetDefaultMarker( app.getStyleSheetDefaultMarker() );
|
|
webView->loadURI( "assets/html/hn_empty_thread.html" );
|
|
|
|
UISceneNode* documentScene = webView->getDocumentSceneNode();
|
|
ASSERT_TRUE( documentScene != nullptr );
|
|
auto* bigboxTd = documentScene->getRoot()->querySelector( "#bigbox > td" );
|
|
auto* bigboxTable = documentScene->getRoot()->querySelector( "#bigbox > td > table" );
|
|
ASSERT_TRUE( bigboxTd != nullptr );
|
|
ASSERT_TRUE( bigboxTable != nullptr );
|
|
|
|
auto* titleCell = documentScene->getRoot()->querySelector( ".title" );
|
|
ASSERT_TRUE( titleCell != nullptr );
|
|
ASSERT_TRUE( titleCell->isType( UI_TYPE_RICHTEXT ) );
|
|
|
|
const Color expectedTitleColor( "#828282" );
|
|
for ( int i = 0;
|
|
i < 120 && titleCell->asType<UIRichText>()->getFontColor() != expectedTitleColor; ++i ) {
|
|
win->getInput()->update();
|
|
SceneManager::instance()->update();
|
|
Sys::sleep( Milliseconds( 1 ) );
|
|
}
|
|
|
|
ASSERT_TRUE( titleCell->asType<UIRichText>()->getFontColor() == expectedTitleColor );
|
|
win->getInput()->update();
|
|
SceneManager::instance()->update();
|
|
|
|
const Float initialTdHeight = bigboxTd->getPixelsSize().getHeight();
|
|
const Float initialTableHeight = bigboxTable->getPixelsSize().getHeight();
|
|
|
|
EXPECT_GT( initialTdHeight, 0.f );
|
|
EXPECT_GT( initialTableHeight, 0.f );
|
|
|
|
win->setSize( win->getWidth(), win->getHeight() + 90 );
|
|
win->getInput()->update();
|
|
SceneManager::instance()->update();
|
|
|
|
win->setSize( win->getWidth(), win->getHeight() - 90 );
|
|
win->getInput()->update();
|
|
SceneManager::instance()->update();
|
|
|
|
// These tests are currently unstable, most likely due to a bug in UIHTMLTextArea
|
|
// EXPECT_NEAR( initialTdHeight, bigboxTd->getPixelsSize().getHeight(), 0.1f );
|
|
// EXPECT_NEAR( initialTableHeight, bigboxTable->getPixelsSize().getHeight(), 0.1f );
|
|
}
|
|
|
|
UTEST( UIHTML, ImageCSSWidthOverridesHTMLWidthAttribute ) {
|
|
auto win = Engine::instance()->createWindow(
|
|
WindowSettings( 1024, 768, "img css width overrides html width attr", WindowStyle::Default,
|
|
WindowBackend::Default, 32, {}, 1, false, true ),
|
|
ContextSettings( false, 0, 0, GLv_default, true, false ) );
|
|
|
|
UISceneNode* sceneNode = init_test_inline_block();
|
|
sceneNode->setURI( "file://" + Sys::getProcessPath() + "assets/html/" );
|
|
|
|
std::string html;
|
|
FileSystem::fileGet( "assets/html/image_width_2.html", html );
|
|
sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( html ) );
|
|
|
|
win->getInput()->update();
|
|
SceneManager::instance()->update();
|
|
sceneNode->updateDirtyLayouts();
|
|
|
|
auto images = sceneNode->getRoot()->findAllByTag( "img" );
|
|
ASSERT_GT( images.size(), (size_t)0 );
|
|
auto* imgWidget = images[0]->asType<UIWidget>();
|
|
ASSERT_TRUE( imgWidget != nullptr );
|
|
|
|
auto mains = sceneNode->getRoot()->findAllByTag( "main" );
|
|
ASSERT_GT( mains.size(), (size_t)0 );
|
|
auto* mainWidget = mains[0]->asType<UIWidget>();
|
|
ASSERT_TRUE( mainWidget != nullptr );
|
|
|
|
Float imgWidth = imgWidget->getPixelsSize().getWidth();
|
|
Float imgHeight = imgWidget->getPixelsSize().getHeight();
|
|
Float mainWidth = mainWidget->getPixelsSize().getWidth();
|
|
|
|
EXPECT_GT( imgWidth, 0.f );
|
|
EXPECT_GT( imgHeight, 0.f );
|
|
|
|
// CSS width: 100% should override HTML width="1500"
|
|
// Image should be no wider than its container
|
|
EXPECT_LE( imgWidth, mainWidth );
|
|
|
|
// Image should NOT be 1500 (the HTML attribute value)
|
|
EXPECT_LT( imgWidth, 1500.f );
|
|
|
|
Engine::destroySingleton();
|
|
}
|
|
|
|
UTEST( UIHTML, ImageMaxWidthConstrainsWebpWithHeightAuto ) {
|
|
auto win = Engine::instance()->createWindow(
|
|
WindowSettings( 1024, 768, "img max-width constrains webp with height auto",
|
|
WindowStyle::Default, WindowBackend::Default, 32, {}, 1, false, true ),
|
|
ContextSettings( false, 0, 0, GLv_default, true, false ) );
|
|
|
|
UISceneNode* sceneNode = init_test_inline_block();
|
|
sceneNode->setURI( "file://" + Sys::getProcessPath() + "assets/html/" );
|
|
|
|
std::string html;
|
|
FileSystem::fileGet( "assets/html/image_width_3.html", html );
|
|
sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( html ) );
|
|
|
|
win->getInput()->update();
|
|
SceneManager::instance()->update();
|
|
sceneNode->updateDirtyLayouts();
|
|
|
|
auto articles = sceneNode->getRoot()->findAllByTag( "article" );
|
|
ASSERT_GT( articles.size(), (size_t)0 );
|
|
auto* articleWidget = articles[0]->asType<UIWidget>();
|
|
ASSERT_TRUE( articleWidget != nullptr );
|
|
|
|
auto images = sceneNode->getRoot()->findAllByTag( "img" );
|
|
ASSERT_GT( images.size(), (size_t)0 );
|
|
auto* imgWidget = images[0]->asType<UIWidget>();
|
|
ASSERT_TRUE( imgWidget != nullptr );
|
|
|
|
EXPECT_GT( articleWidget->getPixelsSize().getWidth(), 0.f );
|
|
EXPECT_GT( articleWidget->getPixelsSize().getHeight(), 0.f );
|
|
EXPECT_LE( articleWidget->getPixelsSize().getWidth(), 1140.f );
|
|
|
|
Float imgWidth = imgWidget->getPixelsSize().getWidth();
|
|
Float imgHeight = imgWidget->getPixelsSize().getHeight();
|
|
|
|
EXPECT_GT( imgWidth, 0.f );
|
|
EXPECT_GT( imgHeight, 0.f );
|
|
EXPECT_LE( imgWidth, articleWidget->getPixelsSize().getWidth() );
|
|
|
|
// max-width: 100% should prevent the image from using the HTML width="2560"
|
|
EXPECT_LT( imgWidth, 2560.f );
|
|
|
|
// height should be proportional to width (aspect ratio: 1436/2560)
|
|
Float expectedRatio = 1436.f / 2560.f;
|
|
Float actualRatio = imgHeight / imgWidth;
|
|
EXPECT_GT( actualRatio, expectedRatio * 0.9f );
|
|
EXPECT_LT( actualRatio, expectedRatio * 1.1f );
|
|
|
|
Engine::destroySingleton();
|
|
}
|
|
|
|
// Applies width/height/max-width to an image inside a single attributes
|
|
// transaction in the given order, then returns the final pixels size. The reset
|
|
// pass restores the image to a known base state (no fixed dimensions and no
|
|
// max-width constraint) before the target order is applied. Returns Sizef::Zero
|
|
// when the fixture image cannot be found.
|
|
static Sizef applyImageSizingOrder( UISceneNode* sceneNode,
|
|
const std::vector<std::string>& order ) {
|
|
auto images = sceneNode->getRoot()->findAllByTag( "img" );
|
|
if ( images.size() != 1 )
|
|
return Sizef::Zero;
|
|
auto* img = images[0]->asType<UIHTMLImage>();
|
|
if ( !img )
|
|
return Sizef::Zero;
|
|
|
|
img->beginAttributesTransaction();
|
|
img->applyProperty( StyleSheetProperty( "width", "auto" ) );
|
|
img->applyProperty( StyleSheetProperty( "height", "auto" ) );
|
|
img->applyProperty( StyleSheetProperty( "max-width", "none" ) );
|
|
img->endAttributesTransaction();
|
|
img->updateLayout();
|
|
|
|
img->beginAttributesTransaction();
|
|
for ( const auto& name : order ) {
|
|
if ( name == "width" )
|
|
img->applyProperty( StyleSheetProperty( "width", "2560px" ) );
|
|
else if ( name == "height" )
|
|
img->applyProperty( StyleSheetProperty( "height", "auto" ) );
|
|
else
|
|
img->applyProperty( StyleSheetProperty( "max-width", "100%" ) );
|
|
}
|
|
img->endAttributesTransaction();
|
|
img->updateLayout();
|
|
|
|
return img->getPixelsSize();
|
|
}
|
|
|
|
UTEST( UIHTML, ImagePropertyApplicationOrderIndependent ) {
|
|
auto win = Engine::instance()->createWindow(
|
|
WindowSettings( 1024, 768, "img sizing property order", WindowStyle::Default,
|
|
WindowBackend::Default, 32, {}, 1, false, true ),
|
|
ContextSettings( false, 0, 0, GLv_default, true, false ) );
|
|
|
|
UISceneNode* sceneNode = init_test_inline_block();
|
|
sceneNode->setURI( "file://" + Sys::getProcessPath() + "assets/html/" );
|
|
|
|
const std::string html = R"html(
|
|
<!doctype html>
|
|
<html>
|
|
<head><style>body { margin: 0; } div { width: 800px; }</style></head>
|
|
<body><div><img src="image_width_3.webp" width="2560" height="1436"></div></body>
|
|
</html>
|
|
)html";
|
|
sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( html ) );
|
|
win->getInput()->update();
|
|
SceneManager::instance()->update();
|
|
sceneNode->updateDirtyLayouts();
|
|
|
|
const Sizef base = applyImageSizingOrder( sceneNode, { "width", "height", "max-width" } );
|
|
EXPECT_GT( base.getWidth(), 0.f );
|
|
EXPECT_GT( base.getHeight(), 0.f );
|
|
// max-width:100% constrains the image to the 800px containing block, and
|
|
// height:auto keeps the 1436/2560 aspect ratio.
|
|
EXPECT_LE( base.getWidth(), 800.f );
|
|
EXPECT_NEAR( base.getHeight() / base.getWidth(), 1436.f / 2560.f, 0.01f );
|
|
|
|
const std::vector<std::vector<std::string>> orders = {
|
|
{ "max-width", "height", "width" },
|
|
{ "height", "width", "max-width" },
|
|
{ "max-width", "width", "height" },
|
|
};
|
|
for ( const auto& order : orders ) {
|
|
const Sizef other = applyImageSizingOrder( sceneNode, order );
|
|
EXPECT_NEAR( other.getWidth(), base.getWidth(), 1.f );
|
|
EXPECT_NEAR( other.getHeight(), base.getHeight(), 1.f );
|
|
}
|
|
|
|
Engine::destroySingleton();
|
|
}
|
|
|
|
UTEST( UIHTML, ImageHeightAutoMaxWidthStyleStateTransition ) {
|
|
auto win = Engine::instance()->createWindow(
|
|
WindowSettings( 1024, 768, "img style state transition", WindowStyle::Default,
|
|
WindowBackend::Default, 32, {}, 1, false, true ),
|
|
ContextSettings( false, 0, 0, GLv_default, true, false ) );
|
|
|
|
UISceneNode* sceneNode = init_test_inline_block();
|
|
sceneNode->setURI( "file://" + Sys::getProcessPath() + "assets/html/" );
|
|
|
|
const std::string html = R"html(
|
|
<!doctype html>
|
|
<html>
|
|
<head><style>body { margin: 0; } div { width: 800px; }</style></head>
|
|
<body><div><img src="image_width_3.webp" width="2560" height="1436"></div></body>
|
|
</html>
|
|
)html";
|
|
sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( html ) );
|
|
win->getInput()->update();
|
|
SceneManager::instance()->update();
|
|
sceneNode->updateDirtyLayouts();
|
|
|
|
auto images = sceneNode->getRoot()->findAllByTag( "img" );
|
|
ASSERT_EQ( images.size(), (size_t)1 );
|
|
auto* img = images[0]->asType<UIHTMLImage>();
|
|
ASSERT_TRUE( img != nullptr );
|
|
|
|
// HTML attributes alone keep the fixed 2560x1436 box.
|
|
EXPECT_NEAR( img->getPixelsSize().getWidth(), 2560.f, 1.f );
|
|
EXPECT_NEAR( img->getPixelsSize().getHeight(), 1436.f, 1.f );
|
|
|
|
const std::string rule = "img { height: auto; max-width: 100%; }";
|
|
|
|
// Apply height:auto + max-width:100% through a style state change.
|
|
sceneNode->setStyleSheet( rule );
|
|
sceneNode->updateDirtyLayouts();
|
|
const Float constrainedWidth = img->getPixelsSize().getWidth();
|
|
const Float constrainedHeight = img->getPixelsSize().getHeight();
|
|
EXPECT_LE( constrainedWidth, 800.f );
|
|
EXPECT_NEAR( constrainedHeight / constrainedWidth, 1436.f / 2560.f, 0.01f );
|
|
|
|
// Remove the rule, then reapply it: the final geometry must be identical.
|
|
sceneNode->setStyleSheet( CSS::StyleSheet() );
|
|
sceneNode->updateDirtyLayouts();
|
|
sceneNode->setStyleSheet( rule );
|
|
sceneNode->updateDirtyLayouts();
|
|
EXPECT_NEAR( img->getPixelsSize().getWidth(), constrainedWidth, 1.f );
|
|
EXPECT_NEAR( img->getPixelsSize().getHeight(), constrainedHeight, 1.f );
|
|
|
|
Engine::destroySingleton();
|
|
}
|