diff --git a/bin/assets/ui/breeze.css b/bin/assets/ui/breeze.css index 46e6428f2..a5af66627 100644 --- a/bin/assets/ui/breeze.css +++ b/bin/assets/ui/breeze.css @@ -171,6 +171,19 @@ MarkdownView blockquote { margin-top: 0; } +MarkdownView ul, +MarkdownView ol { + padding-left: 0; +} + +MarkdownView ul ul, +MarkdownView ul ol, +MarkdownView ol ul, +MarkdownView ol ol { + margin-top: 0; + margin-bottom: 0; +} + MarkdownView a { color: var(--primary); selection-color: var(--font-selected-pressed); diff --git a/include/eepp/ui/csslayouttypes.hpp b/include/eepp/ui/csslayouttypes.hpp index e7860a0a9..e301aa4b7 100644 --- a/include/eepp/ui/csslayouttypes.hpp +++ b/include/eepp/ui/csslayouttypes.hpp @@ -10,6 +10,7 @@ enum class CSSDisplay { Inline, Block, InlineBlock, + ListItem, Flex, None, Table, diff --git a/src/eepp/graphics/csslayouttypes.cpp b/src/eepp/graphics/csslayouttypes.cpp index 995ca39d6..38b5acd4a 100644 --- a/src/eepp/graphics/csslayouttypes.cpp +++ b/src/eepp/graphics/csslayouttypes.cpp @@ -8,6 +8,8 @@ std::string CSSDisplayHelper::toString( CSSDisplay display ) { return "inline"; case CSSDisplay::InlineBlock: return "inline-block"; + case CSSDisplay::ListItem: + return "list-item"; case CSSDisplay::Flex: return "flex"; case CSSDisplay::None: @@ -36,6 +38,8 @@ CSSDisplay CSSDisplayHelper::fromString( std::string_view val ) { display = CSSDisplay::Inline; else if ( val == "inline-block" ) display = CSSDisplay::InlineBlock; + else if ( val == "list-item" ) + display = CSSDisplay::ListItem; else if ( val == "none" ) display = CSSDisplay::None; else if ( val == "table" ) diff --git a/src/eepp/ui/uihtmllistitem.cpp b/src/eepp/ui/uihtmllistitem.cpp index 914981bdf..f43f330a1 100644 --- a/src/eepp/ui/uihtmllistitem.cpp +++ b/src/eepp/ui/uihtmllistitem.cpp @@ -9,7 +9,9 @@ UIHTMLListItem* UIHTMLListItem::New() { return eeNew( UIHTMLListItem, () ); } -UIHTMLListItem::UIHTMLListItem() : UIRichText( "li" ) {} +UIHTMLListItem::UIHTMLListItem() : UIRichText( "li" ) { + mDisplay = CSSDisplay::ListItem; +} void UIHTMLListItem::setListStyleType( CSSListStyleType type ) { if ( mListStyleType != type ) { @@ -35,7 +37,7 @@ bool UIHTMLListItem::isType( const Uint32& type ) const { void UIHTMLListItem::draw() { UIRichText::draw(); - if ( mVisible && 0.f != mAlpha ) { + if ( mVisible && 0.f != mAlpha && mDisplay == CSSDisplay::ListItem ) { const FontStyleConfig& style = mRichText.getFontStyleConfig(); Float fontSize = style.CharacterSize; Float offset = 0.25f * fontSize; diff --git a/src/eepp/ui/uihtmlwidget.cpp b/src/eepp/ui/uihtmlwidget.cpp index dc86dd84f..d6758c853 100644 --- a/src/eepp/ui/uihtmlwidget.cpp +++ b/src/eepp/ui/uihtmlwidget.cpp @@ -46,6 +46,13 @@ void UIHTMLWidget::onDisplayChange() { void UIHTMLWidget::setDisplay( CSSDisplay display ) { if ( mDisplay != display ) { mDisplay = display; + if ( mDisplay == CSSDisplay::InlineBlock || mDisplay == CSSDisplay::Inline ) { + if ( getLayoutWidthPolicy() == SizePolicy::MatchParent ) + setLayoutWidthPolicy( SizePolicy::WrapContent ); + } else if ( mDisplay == CSSDisplay::Block || mDisplay == CSSDisplay::ListItem ) { + if ( getLayoutWidthPolicy() == SizePolicy::WrapContent ) + setLayoutWidthPolicy( SizePolicy::MatchParent ); + } onDisplayChange(); } } diff --git a/src/eepp/ui/uilayoutermanager.cpp b/src/eepp/ui/uilayoutermanager.cpp index 21b73c209..f917205d2 100644 --- a/src/eepp/ui/uilayoutermanager.cpp +++ b/src/eepp/ui/uilayoutermanager.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include @@ -12,10 +13,14 @@ UILayouter* UILayouterManager::create( CSSDisplay display, UIWidget* container ) switch ( display ) { case CSSDisplay::Block: case CSSDisplay::TableCell: + case CSSDisplay::InlineBlock: + case CSSDisplay::ListItem: + case CSSDisplay::Flex: return eeNew( BlockLayouter, ( container ) ); case CSSDisplay::Inline: - case CSSDisplay::InlineBlock: - return eeNew( InlineLayouter, ( container ) ); + if ( container->isType( UI_TYPE_TEXTSPAN ) ) + return eeNew( InlineLayouter, ( container ) ); + return eeNew( BlockLayouter, ( container ) ); case CSSDisplay::Table: return eeNew( TableLayouter, ( container ) ); case CSSDisplay::None: diff --git a/src/eepp/ui/uirichtext.cpp b/src/eepp/ui/uirichtext.cpp index d641a2e51..4630f03a3 100644 --- a/src/eepp/ui/uirichtext.cpp +++ b/src/eepp/ui/uirichtext.cpp @@ -1,5 +1,6 @@ #include #include +#include #include #include #include @@ -110,10 +111,13 @@ UIRichText* UIRichText::NewBr() { static void applyDefaultBlockMargins( UIWidget* widget, const std::string& tag ) { static const UnorderedMap> defaults = { - { "h1", { 0.67f, 0.67f } }, { "h2", { 0.83f, 0.83f } }, { "h3", { 1.00f, 1.00f } }, - { "h4", { 1.33f, 1.33f } }, { "h5", { 1.67f, 1.67f } }, { "h6", { 2.33f, 2.33f } }, - { "p", { 1.00f, 1.00f } }, { "pre", { 1.00f, 1.00f } }, { "blockquote", { 1.00f, 1.00f } }, - { "hr", { 0.50f, 0.50f } }, + { "h1", { 0.67f, 0.67f } }, { "h2", { 0.83f, 0.83f } }, + { "h3", { 1.00f, 1.00f } }, { "h4", { 1.33f, 1.33f } }, + { "h5", { 1.67f, 1.67f } }, { "h6", { 2.33f, 2.33f } }, + { "p", { 1.00f, 1.00f } }, { "pre", { 1.00f, 1.00f } }, + { "blockquote", { 1.00f, 1.00f } }, { "hr", { 0.50f, 0.50f } }, + { "ul", { 1.00f, 1.00f } }, { "ol", { 1.00f, 1.00f } }, + { "dl", { 1.00f, 1.00f } }, { "body", { 0.67f, 0.67f } }, }; auto it = defaults.find( tag ); if ( it != defaults.end() ) { @@ -143,20 +147,21 @@ UIRichText* UIRichText::NewWithTag( const std::string& tag ) { UIRichText::UIRichText( const std::string& tag ) : UIHTMLWidget( tag ) { mFlags |= UI_HTML_ELEMENT | UI_LOADS_ITS_CHILDREN | UI_OWNS_CHILDREN_POSITION; - UITheme* theme = getUISceneNode()->getUIThemeManager()->getDefaultTheme(); + UISceneNode* sceneNode = + getUISceneNode() ? getUISceneNode() : SceneManager::instance()->getUISceneNode(); + UITheme* theme = sceneNode ? sceneNode->getUIThemeManager()->getDefaultTheme() : nullptr; if ( NULL != theme && NULL != theme->getDefaultFont() ) { mRichText.getFontStyleConfig().Font = theme->getDefaultFont(); - } else if ( NULL != getUISceneNode()->getUIThemeManager()->getDefaultFont() ) { - mRichText.getFontStyleConfig().Font = - getUISceneNode()->getUIThemeManager()->getDefaultFont(); + } else if ( sceneNode && NULL != sceneNode->getUIThemeManager()->getDefaultFont() ) { + mRichText.getFontStyleConfig().Font = sceneNode->getUIThemeManager()->getDefaultFont(); } if ( NULL != theme ) { mRichText.getFontStyleConfig().CharacterSize = theme->getDefaultFontSize(); - } else { + } else if ( sceneNode ) { mRichText.getFontStyleConfig().CharacterSize = - getUISceneNode()->getUIThemeManager()->getDefaultFontSize(); + sceneNode->getUIThemeManager()->getDefaultFontSize(); } setLayoutSizePolicy( SizePolicy::MatchParent, SizePolicy::WrapContent ); @@ -520,6 +525,8 @@ void UIRichText::loadFromXmlNode( const pugi::xml_node& node ) { editor->applyProperty( langIt->second ); } } + } else if ( String::iequals( child.name(), "style" ) ) { + getUISceneNode()->loadNode( child, this, 0 ); } else if ( String::iequals( child.name(), "script" ) ) { // No plans to support it continue; @@ -637,6 +644,13 @@ void UIRichText::rebuildRichText( UILayout* container, RichText& richText, Intri } else { Rectf margin = widget->getLayoutPixelsMargin(); bool isBlock = widget->getLayoutWidthPolicy() == SizePolicy::MatchParent; + if ( widget->isType( UI_TYPE_HTML_WIDGET ) ) { + CSSDisplay display = widget->asType()->getDisplay(); + if ( display == CSSDisplay::Inline || display == CSSDisplay::InlineBlock ) + isBlock = false; + else if ( display == CSSDisplay::ListItem ) + isBlock = true; + } if ( mode == IntrinsicMode::None ) { if ( isBlock ) { diff --git a/src/eepp/ui/uiwidgetcreator.cpp b/src/eepp/ui/uiwidgetcreator.cpp index 852b48a1f..45dc1bfc6 100644 --- a/src/eepp/ui/uiwidgetcreator.cpp +++ b/src/eepp/ui/uiwidgetcreator.cpp @@ -160,15 +160,20 @@ void UIWidgetCreator::createBaseWidgetList() { registeredWidget["br"] = UIRichText::NewBr; registeredWidget["hr"] = UIRichText::NewHr; registeredWidget["ul"] = [] { - auto* w = UILinearLayout::NewVerticalWidthMatchParent( "ul" ); - w->applyProperty( StyleSheetProperty( "margin-top", "0.67em" ) ); - w->applyProperty( StyleSheetProperty( "margin-bottom", "0.67em" ) ); + auto* w = UIRichText::NewWithTag( "ul" ); + w->applyProperty( StyleSheetProperty( "padding-left", "40dp" ) ); return w; }; registeredWidget["ol"] = [] { - auto* w = UILinearLayout::NewVerticalWidthMatchParent( "ol" ); - w->applyProperty( StyleSheetProperty( "margin-top", "0.67em" ) ); - w->applyProperty( StyleSheetProperty( "margin-bottom", "0.67em" ) ); + auto* w = UIRichText::NewWithTag( "ol" ); + w->applyProperty( StyleSheetProperty( "padding-left", "40dp" ) ); + return w; + }; + registeredWidget["dl"] = [] { return UIRichText::NewWithTag( "dl" ); }; + registeredWidget["dt"] = [] { return UIRichText::NewWithTag( "dt" ); }; + registeredWidget["dd"] = [] { + auto* w = UIRichText::NewWithTag( "dd" ); + w->applyProperty( StyleSheetProperty( "margin-left", "40dp" ) ); return w; }; registeredWidget["li"] = UIHTMLListItem::New; diff --git a/src/examples/ui_markdownview/ui_markdownview.cpp b/src/examples/ui_markdownview/ui_markdownview.cpp index 6b4195c5d..bdb6893c7 100644 --- a/src/examples/ui_markdownview/ui_markdownview.cpp +++ b/src/examples/ui_markdownview/ui_markdownview.cpp @@ -14,6 +14,8 @@ EE_MAIN_FUNC int main( int, char** ) { This is a **bold** text and this is an *italic* text. * Item 1 + * Sub Item 1 + * Sub Item 2 * Item 2 * Item 3 diff --git a/src/tests/unit_tests/uihtml_position_tests.cpp b/src/tests/unit_tests/uihtml_position_tests.cpp index a8554c750..52572fddb 100644 --- a/src/tests/unit_tests/uihtml_position_tests.cpp +++ b/src/tests/unit_tests/uihtml_position_tests.cpp @@ -1,6 +1,16 @@ #include "utest.h" -#include +#include +#include #include +#include +#include +#include +#include +#include +#include +#include +#include +#include using namespace EE; using namespace EE::UI; @@ -231,9 +241,6 @@ UTEST( UIHTMLWidget, positionOutOfFlow_PercentageAndMargin ) { Engine::destroySingleton(); } -#include -#include - UTEST( UIHTMLWidget, positionOutOfFlow_ComplexHTML ) { init_ui_test(); UISceneNode* sceneNode = SceneManager::instance()->getUISceneNode(); diff --git a/src/tests/unit_tests/uihtml_tests.cpp b/src/tests/unit_tests/uihtml_tests.cpp index bf69198b3..d52ade259 100644 --- a/src/tests/unit_tests/uihtml_tests.cpp +++ b/src/tests/unit_tests/uihtml_tests.cpp @@ -870,3 +870,224 @@ UTEST( UIBorder, renderingVariations2 ) { Engine::destroySingleton(); } + +static UISceneNode* init_test_inline_block() { + FontTrueType* font = nullptr; + FileSystem::changeWorkingDirectory( Sys::getProcessPath() ); + font = FontTrueType::New( "NotoSans-Regular" ); + font->loadFromFile( "../assets/fonts/NotoSans-Regular.ttf" ); + FontFamily::loadFromRegular( font ); + UISceneNode* sceneNode = UISceneNode::New(); + SceneManager::instance()->add( sceneNode ); + SceneManager::instance()->setCurrentUISceneNode( sceneNode ); + UIThemeManager* themeManager = sceneNode->getUIThemeManager(); + themeManager->setDefaultFont( font ); + UITheme* theme = UITheme::New( "default", "default" ); + theme->setDefaultFont( font ); + themeManager->setDefaultTheme( theme ); + themeManager->applyDefaultTheme( sceneNode->getRoot() ); + return sceneNode; +} + +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( + + + + + + + + + +)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()->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, 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( +
    +
  • Item 1
  • +
  • Item 2
  • +
+)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()->getDisplay(), CSSDisplay::ListItem ); + EXPECT_GT( li->getChildCount(), (size_t)0 ); + EXPECT_TRUE( li->asType()->getRichTextPtr()->getFontStyleConfig().Font != + nullptr ); + EXPECT_GT( li->asType()->getRichTextPtr()->getFontStyleConfig().CharacterSize, + 0 ); + EXPECT_GT( li->asType()->getRichTextPtr()->getSize().getWidth(), 0 ); + EXPECT_GT( li->asType()->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( +
    +
  • Item 1
  • +
  • Item 2
  • +
+)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()->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( +
+
+
+
+)HTML"; + + sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( html ) ); + sceneNode->update( Seconds( 1 ) ); + + auto d1 = sceneNode->getRoot()->find( "d1" )->asType(); + auto d2 = sceneNode->getRoot()->find( "d2" )->asType(); + 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, InlineBlockMixedContent ) { + Engine::instance()->createWindow( WindowSettings( 1024, 768, "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( +
+ Some text +
+ more text +
+)HTML"; + + sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( html ) ); + sceneNode->update( Seconds( 1 ) ); + + auto ib = sceneNode->getRoot()->find( "ib" )->asType(); + 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(); +}