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 @@
+
+
+
+
+
+
+
+ - Everything in Free, plus:
+
+ -
+
+ Follow up to 1,024 sites
+
+
+ -
+
+ Sites updated up to 5x more often
+
+
+ -
+
+ River of News (reading by folder)
+
+
+ -
+
+ Natural language text and image classifiers
+
+
+
+
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.