Add anonymous flex item wrap support.

This commit is contained in:
Martín Lucas Golini
2026-05-30 02:52:23 -03:00
parent 8be05aabdd
commit abfd10bee4
5 changed files with 252 additions and 36 deletions

View File

@@ -330,14 +330,11 @@ void FlexLayouter::measureFlexItems( const Axis& mainAxis, const Axis& crossAxis
item.minMainSize =
item.widget->asType<UIHTMLWidget>()->getLayouter()->getMinIntrinsicWidth();
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<UITextNode>()->getText(), fontConfig );
} else {
item.minMainSize = item.widget->getPixelsSize().getWidth();
}
// Text node with wrapping: minimum size = 0, since text can wrap
// to any width and the cross size grows to accommodate the content.
// Without wrapping, the minimum would be the full text width,
// which prevents any flex-shrink.
item.minMainSize = 0.f;
} else
item.minMainSize = item.widget->getPixelsSize().getWidth();
if ( item.minMainSize < 0.f )
@@ -724,6 +721,42 @@ void FlexLayouter::resolveCrossSizes( FlexLine& line, const Axis& crossAxis,
}
}
// For text nodes (anonymous flex items), configure a cached Text object for
// multi-line word wrapping. In row direction, measure wrapped text height at
// the resolved main size and use it as the cross size.
for ( size_t idx : line.itemIndices ) {
auto& item = mItems[idx];
if ( item.widget->isType( UI_TYPE_TEXTNODE ) ) {
auto* textNode = item.widget->asType<UITextNode>();
if ( !textNode->getText().empty() ) {
FontStyleConfig fontConfig;
if ( getFontStyleFromAncestor( mContainer, fontConfig ) ) {
Graphics::Text& flexText = textNode->getFlexText();
flexText.setStyleConfig( fontConfig );
flexText.setString( textNode->getText() );
flexText.setLineWrapMode( LineWrapMode::Word );
Float maxWidth;
if ( mainAxis.horizontal ) {
maxWidth = item.targetMainSize;
} else {
// Column: wrap at tentative cross-axis width
maxWidth = item.widget->getPixelsSize().getWidth();
}
if ( maxWidth <= 0.f )
maxWidth = 10000.f;
flexText.setMaxWrapWidth( maxWidth );
if ( mainAxis.horizontal ) {
Float wrappedHeight = flexText.getTextHeight();
if ( wrappedHeight <= 0.f )
wrappedHeight =
(Float)fontConfig.Font->getFontHeight( fontConfig.CharacterSize );
item.widget->setInternalPixelsHeight( wrappedHeight );
}
}
}
}
}
Float maxCross = 0.f;
for ( size_t idx : line.itemIndices ) {
auto& item = mItems[idx];

View File

@@ -29,28 +29,33 @@ void UITextNode::draw() {
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<UIHTMLWidget>()->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<UIRichText>()->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 );
if ( mFlexText.getFont() ) {
mFlexText.setMaxWrapWidth( getPixelsSize().getWidth() );
Float alpha = getAlpha();
if ( alpha < 1.f )
mFlexText.setAlpha( (Uint8)( 255.f * alpha ) );
mFlexText.draw( mScreenPos.x, mScreenPos.y );
} else {
// Fallback: Text not yet configured by flex layouter — draw single-line
Node* n = parent;
while ( n ) {
if ( n->isType( UI_TYPE_RICHTEXT ) ) {
auto& fontConfig = n->asType<UIRichText>()->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;
}
break;
n = n->getParent();
}
n = n->getParent();
}
}
}

View File

@@ -1588,3 +1588,38 @@ UTEST( FlexContainer, anonymousTextNodeWithFixedSibling ) {
Engine::destroySingleton();
}
UTEST( FlexContainer, anonymousTextNodeWrapping ) {
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( 100, 200 );
flex->setLayoutSizePolicy( SizePolicy::Fixed, SizePolicy::Fixed );
flex->applyProperty( StyleSheetProperty( "font-family", "NotoSans-Regular" ) );
flex->applyProperty( StyleSheetProperty( "font-size", "16dp" ) );
flex->applyProperty( StyleSheetProperty( "align-items", "flex-start" ) );
// Long text that should wrap within the 100px-wide flex container
UITextNode* textNode = UITextNode::New();
textNode->setParent( flex );
textNode->setText( "This is a very long text that should wrap to multiple lines inside the narrow flex container." );
sceneNode->updateDirtyLayouts();
// The font height for NotoSans at 16dp is ~22px. If the text wraps, the total
// height should be at least 2x the single-line font height.
FontStyleConfig fontConfig = flex->getRichText().getFontStyleConfig();
Float fontHeight = (Float)fontConfig.Font->getFontHeight( fontConfig.CharacterSize );
EXPECT_TRUE( textNode->getPixelsSize().getWidth() > 0.f );
EXPECT_TRUE( textNode->getPixelsSize().getHeight() >= fontHeight * 2.f );
EXPECT_TRUE( textNode->getPixelsSize().getWidth() <= 100.f );
Engine::destroySingleton();
}