From 8be05aabdd2a7e26c74c9a2da662603565b7bf11 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=ADn=20Lucas=20Golini?= Date: Sat, 30 May 2026 02:37:22 -0300 Subject: [PATCH] Initial implementation to support rendering text nodes as anonymous flex items. This does not support wrapping yet. --- bin/unit_tests/assets/html/flex_textnode.html | 76 ++++++++++++++++ src/eepp/ui/flexlayouter.cpp | 88 ++++++++++++++++++- src/eepp/ui/uitextnode.cpp | 30 ++++++- src/tests/unit_tests/uihtml_flex_test.cpp | 78 ++++++++++++++++ src/tests/unit_tests/uihtml_tests.cpp | 1 - 5 files changed, 269 insertions(+), 4 deletions(-) create mode 100644 bin/unit_tests/assets/html/flex_textnode.html diff --git a/bin/unit_tests/assets/html/flex_textnode.html b/bin/unit_tests/assets/html/flex_textnode.html new file mode 100644 index 000000000..ca0eb562e --- /dev/null +++ b/bin/unit_tests/assets/html/flex_textnode.html @@ -0,0 +1,76 @@ + + + + + + + + + diff --git a/src/eepp/ui/flexlayouter.cpp b/src/eepp/ui/flexlayouter.cpp index 104ec9cab..e82a99843 100644 --- a/src/eepp/ui/flexlayouter.cpp +++ b/src/eepp/ui/flexlayouter.cpp @@ -1,11 +1,14 @@ +#include #include #include #include #include #include +#include #include #include #include +#include namespace EE { namespace UI { @@ -81,6 +84,33 @@ FlexLayouter::Axis FlexLayouter::getCrossAxis( CSSFlexDirection direction ) cons return { false, false }; } +namespace { + +bool getFontStyleFromAncestor( UIWidget* widget, FontStyleConfig& outConfig ) { + Node* n = widget; + while ( n ) { + if ( n->isType( UI_TYPE_RICHTEXT ) ) { + outConfig = n->asType()->getRichText().getFontStyleConfig(); + return outConfig.Font != nullptr; + } + n = n->getParent(); + } + // Fallback: try the scene node's default font + UISceneNode* sceneNode = widget->getUISceneNode(); + if ( sceneNode ) { + Font* defaultFont = sceneNode->getUIThemeManager()->getDefaultFont(); + if ( defaultFont ) { + outConfig = FontStyleConfig(); + outConfig.Font = defaultFont; + outConfig.CharacterSize = 12; + return true; + } + } + return false; +} + +} // namespace + void FlexLayouter::readContainerStyle( CSSFlexDirection& direction, CSSFlexWrap& wrap, CSSJustifyContent& justify, CSSAlignItems& alignItems, CSSAlignContent& alignContent, Float& columnGap, @@ -129,6 +159,17 @@ void FlexLayouter::readItemStyle( UIWidget* child, FlexItem& item ) { Float FlexLayouter::resolveFlexBasis( UIWidget* child, CSSFlexDirection, Float flexBasisValue, bool flexBasisAuto, const Axis& mainAxis ) { if ( flexBasisAuto ) { + // For text nodes (anonymous flex items), measure text content width + if ( child->isType( UI_TYPE_TEXTNODE ) ) { + auto* textNode = child->asType(); + if ( !textNode->getText().empty() ) { + FontStyleConfig fontConfig; + if ( getFontStyleFromAncestor( mContainer, fontConfig ) ) { + return Text::getTextWidth( textNode->getText(), fontConfig ); + } + } + } + Float intrinsic = 0.f; if ( mainAxis.horizontal && child->getLayoutWidthPolicy() == SizePolicy::Fixed && child->getUIStyle() ) { @@ -258,13 +299,46 @@ void FlexLayouter::measureFlexItems( const Axis& mainAxis, const Axis& crossAxis item.targetMainSize = item.widget->lengthFromValue( *hprop ); } + // For text node flex items, measure text content to determine intrinsic size. + // CSS Flexbox §4: text nodes inside a flex container become anonymous flex items + // whose main size is based on their text content. + if ( item.widget->isType( UI_TYPE_TEXTNODE ) ) { + auto* textNode = item.widget->asType(); + if ( !textNode->getText().empty() ) { + FontStyleConfig fontConfig; + if ( getFontStyleFromAncestor( mContainer, fontConfig ) ) { + Float textWidth = Text::getTextWidth( textNode->getText(), fontConfig ); + Float textHeight = + (Float)fontConfig.Font->getFontHeight( fontConfig.CharacterSize ); + if ( textWidth > 0.f ) { + if ( item.targetMainSize <= 0.f ) + item.targetMainSize = textWidth; + if ( mainAxis.horizontal ) + // cross axis is vertical for row + item.widget->setInternalPixelsHeight( textHeight ); + else + item.widget->setInternalPixelsWidth( textWidth ); + } + } + } + } + // Compute min/max main size (CSS §4.5: min-width/min-height default is auto for flex items) if ( mainAxis.horizontal ) { if ( item.widget->isType( UI_TYPE_HTML_WIDGET ) && item.widget->asType()->getLayouter() ) item.minMainSize = item.widget->asType()->getLayouter()->getMinIntrinsicWidth(); - else + else if ( item.widget->isType( UI_TYPE_TEXTNODE ) ) { + // Text node min size = measured text width (content-based minimum) + FontStyleConfig fontConfig; + if ( getFontStyleFromAncestor( mContainer, fontConfig ) ) { + item.minMainSize = Text::getTextWidth( + item.widget->asType()->getText(), fontConfig ); + } else { + item.minMainSize = item.widget->getPixelsSize().getWidth(); + } + } else item.minMainSize = item.widget->getPixelsSize().getWidth(); if ( item.minMainSize < 0.f ) item.minMainSize = 0.f; @@ -296,7 +370,17 @@ void FlexLayouter::measureFlexItems( const Axis& mainAxis, const Axis& crossAxis item.maxMainSize = std::numeric_limits::max(); } } else { - item.minMainSize = item.widget->getPixelsSize().getHeight(); + if ( item.widget->isType( UI_TYPE_TEXTNODE ) ) { + FontStyleConfig fontConfig; + if ( getFontStyleFromAncestor( mContainer, fontConfig ) ) { + item.minMainSize = + (Float)fontConfig.Font->getFontHeight( fontConfig.CharacterSize ); + } else { + item.minMainSize = item.widget->getPixelsSize().getHeight(); + } + } else { + item.minMainSize = item.widget->getPixelsSize().getHeight(); + } if ( item.minMainSize < 0.f ) item.minMainSize = 0.f; diff --git a/src/eepp/ui/uitextnode.cpp b/src/eepp/ui/uitextnode.cpp index b82a480ed..79fec28ff 100644 --- a/src/eepp/ui/uitextnode.cpp +++ b/src/eepp/ui/uitextnode.cpp @@ -1,3 +1,5 @@ +#include +#include #include #include @@ -24,7 +26,33 @@ bool UITextNode::isType( const Uint32& type ) const { } void UITextNode::draw() { - // Text nodes do not draw themselves; their parent handles rendering + if ( mText.empty() ) + return; + + // In flex layout, text nodes are anonymous flex items. The parent flex + // container does not render their text via RichText (it was cleared), + // so the text node must draw itself. + Node* parent = getParent(); + bool isFlexItem = + parent && parent->isType( UI_TYPE_HTML_WIDGET ) && parent->asType()->isFlex(); + if ( isFlexItem ) { + // Walk up parent chain to find a UIRichText ancestor with font info + Node* n = parent; + while ( n ) { + if ( n->isType( UI_TYPE_RICHTEXT ) ) { + auto& fontConfig = n->asType()->getRichText().getFontStyleConfig(); + if ( fontConfig.Font ) { + FontStyleConfig fc = fontConfig; + Float alpha = getAlpha(); + if ( alpha < 1.f ) + fc.FontColor.a = (Uint8)( (Float)fc.FontColor.a * alpha ); + Text::draw( mText, mScreenPos.trunc(), fc ); + } + break; + } + n = n->getParent(); + } + } } std::vector UITextNode::getPropertiesImplemented() const { diff --git a/src/tests/unit_tests/uihtml_flex_test.cpp b/src/tests/unit_tests/uihtml_flex_test.cpp index 57da06844..da8c0fc08 100644 --- a/src/tests/unit_tests/uihtml_flex_test.cpp +++ b/src/tests/unit_tests/uihtml_flex_test.cpp @@ -11,6 +11,7 @@ #include #include #include +#include #include #include #include @@ -1510,3 +1511,80 @@ UTEST( FlexContainer, crossAxisAutoMargins ) { Engine::destroySingleton(); } + +// ───────────────────────────────────────────────────────────────────────────── +// Anonymous Flex Items (text nodes as flex items) +// ───────────────────────────────────────────────────────────────────────────── + +UTEST( FlexContainer, anonymousTextNodeSizing ) { + Engine::instance()->createWindow( WindowSettings( 1024, 650, "Flex Test", WindowStyle::Default, + WindowBackend::Default, 32, {}, 1, false, + true ), + ContextSettings( false, 0, 0, GLv_default, true, false ) ); + init_flex_test(); + UISceneNode* sceneNode = SceneManager::instance()->getUISceneNode(); + + UIRichText* flex = UIRichText::NewWithTag( "div" ); + flex->setParent( sceneNode->getRoot() ); + flex->setDisplay( CSSDisplay::Flex ); + flex->setPixelsSize( 500, 200 ); + flex->setLayoutSizePolicy( SizePolicy::Fixed, SizePolicy::Fixed ); + flex->applyProperty( StyleSheetProperty( "font-family", "NotoSans-Regular" ) ); + flex->applyProperty( StyleSheetProperty( "font-size", "16dp" ) ); + + UIRichText* child1 = UIRichText::NewDiv(); + child1->setParent( flex ); + child1->setPixelsSize( 20, 20 ); + child1->setLayoutSizePolicy( SizePolicy::Fixed, SizePolicy::Fixed ); + + UITextNode* textNode = UITextNode::New(); + textNode->setParent( flex ); + textNode->setText( "Hello flex text" ); + + sceneNode->updateDirtyLayouts(); + + EXPECT_TRUE( textNode->getPixelsSize().getWidth() > 0.f ); + EXPECT_TRUE( textNode->getPixelsSize().getHeight() > 0.f ); + EXPECT_TRUE( textNode->getPixelsPosition().x >= + child1->getPixelsPosition().x + child1->getPixelsSize().getWidth() ); + + Engine::destroySingleton(); +} + +UTEST( FlexContainer, anonymousTextNodeWithFixedSibling ) { + Engine::instance()->createWindow( WindowSettings( 1024, 650, "Flex Test", WindowStyle::Default, + WindowBackend::Default, 32, {}, 1, false, + true ), + ContextSettings( false, 0, 0, GLv_default, true, false ) ); + init_flex_test(); + UISceneNode* sceneNode = SceneManager::instance()->getUISceneNode(); + + UIRichText* flex = UIRichText::NewWithTag( "div" ); + flex->setParent( sceneNode->getRoot() ); + flex->setDisplay( CSSDisplay::Flex ); + flex->setPixelsSize( 400, 100 ); + flex->setLayoutSizePolicy( SizePolicy::Fixed, SizePolicy::Fixed ); + flex->applyProperty( StyleSheetProperty( "font-family", "NotoSans-Regular" ) ); + flex->applyProperty( StyleSheetProperty( "font-size", "14dp" ) ); + flex->applyProperty( StyleSheetProperty( "align-items", "flex-start" ) ); + flex->applyProperty( StyleSheetProperty( "column-gap", "10px" ) ); + + UIRichText* bullet = UIRichText::NewDiv(); + bullet->setParent( flex ); + bullet->setPixelsSize( 8, 8 ); + bullet->setLayoutSizePolicy( SizePolicy::Fixed, SizePolicy::Fixed ); + + UITextNode* textNode = UITextNode::New(); + textNode->setParent( flex ); + textNode->setText( "Follow up to 1,024 sites" ); + + sceneNode->updateDirtyLayouts(); + + EXPECT_TRUE( textNode->getPixelsSize().getWidth() > 0.f ); + EXPECT_TRUE( textNode->getPixelsSize().getHeight() > 0.f ); + // 10px gap + bullet width (8px) + Float expectedMinX = bullet->getPixelsPosition().x + bullet->getPixelsSize().getWidth() + 10.f; + EXPECT_TRUE( textNode->getPixelsPosition().x >= expectedMinX - 1.f ); + + Engine::destroySingleton(); +} diff --git a/src/tests/unit_tests/uihtml_tests.cpp b/src/tests/unit_tests/uihtml_tests.cpp index 4df046e7c..0cbcb3666 100644 --- a/src/tests/unit_tests/uihtml_tests.cpp +++ b/src/tests/unit_tests/uihtml_tests.cpp @@ -2547,7 +2547,6 @@ UTEST( UIHTML, BodyHeightMiscalculationFixture ) { // .links should be at the right side of .wrap, NOT next to .brand. Float wrapW = wrapWidget->getPixelsSize().getWidth(); Float linksW = linksWidget->getPixelsSize().getWidth(); - Float linksRight = linksWidget->getPixelsPosition().x + linksW; EXPECT_GT( linksWidget->getPixelsPosition().x, wrapW * 0.5f ); // .links should NOT occupy all available width — it must be content-sized.