Improvements for ul and ol.

Added a few tags: dl, dt, dd.
Load style tags inside UIRichText blocks.
Added `display: list-item`.
This commit is contained in:
Martín Lucas Golini
2026-04-30 12:58:05 -03:00
parent bcdd88d1e6
commit 6691d9c174
11 changed files with 305 additions and 24 deletions

View File

@@ -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);

View File

@@ -10,6 +10,7 @@ enum class CSSDisplay {
Inline,
Block,
InlineBlock,
ListItem,
Flex,
None,
Table,

View File

@@ -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" )

View File

@@ -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;

View File

@@ -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();
}
}

View File

@@ -3,6 +3,7 @@
#include <eepp/ui/inlinelayouter.hpp>
#include <eepp/ui/nonelayouter.hpp>
#include <eepp/ui/tablelayouter.hpp>
#include <eepp/ui/uitextspan.hpp>
#include <eepp/ui/uilayouter.hpp>
#include <eepp/ui/uilayoutermanager.hpp>
@@ -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:

View File

@@ -1,5 +1,6 @@
#include <eepp/graphics/fontmanager.hpp>
#include <eepp/graphics/text.hpp>
#include <eepp/scene/scenemanager.hpp>
#include <eepp/ui/css/propertydefinition.hpp>
#include <eepp/ui/tools/htmlformatter.hpp>
#include <eepp/ui/uicodeeditor.hpp>
@@ -110,10 +111,13 @@ UIRichText* UIRichText::NewBr() {
static void applyDefaultBlockMargins( UIWidget* widget, const std::string& tag ) {
static const UnorderedMap<std::string, std::pair<Float, Float>> 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<UIHTMLWidget>()->getDisplay();
if ( display == CSSDisplay::Inline || display == CSSDisplay::InlineBlock )
isBlock = false;
else if ( display == CSSDisplay::ListItem )
isBlock = true;
}
if ( mode == IntrinsicMode::None ) {
if ( isBlock ) {

View File

@@ -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;

View File

@@ -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

View File

@@ -1,6 +1,16 @@
#include "utest.h"
#include <eepp/ee.hpp>
#include <eepp/graphics/fontfamily.hpp>
#include <eepp/graphics/fonttruetype.hpp>
#include <eepp/scene/scenemanager.hpp>
#include <eepp/system/filesystem.hpp>
#include <eepp/ui/css/stylesheetparser.hpp>
#include <eepp/ui/tools/htmlformatter.hpp>
#include <eepp/ui/uihtmlwidget.hpp>
#include <eepp/ui/uirichtext.hpp>
#include <eepp/ui/uiscenenode.hpp>
#include <eepp/ui/uithememanager.hpp>
#include <eepp/window/engine.hpp>
#include <eepp/window/window.hpp>
using namespace EE;
using namespace EE::UI;
@@ -231,9 +241,6 @@ UTEST( UIHTMLWidget, positionOutOfFlow_PercentageAndMargin ) {
Engine::destroySingleton();
}
#include <eepp/ui/tools/htmlformatter.hpp>
#include <eepp/ui/css/stylesheetparser.hpp>
UTEST( UIHTMLWidget, positionOutOfFlow_ComplexHTML ) {
init_ui_test();
UISceneNode* sceneNode = SceneManager::instance()->getUISceneNode();

View File

@@ -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(
<!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, 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, 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(
<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();
}