mirror of
https://github.com/SpartanJ/eepp.git
synced 2026-10-01 18:50:16 +03:00
Fix shrink-to-fit floats and atomic inline alignment
Blockify floated inline elements and use wrap-content sizing for width-auto floats so vote columns do not stretch across their containing block. Recompute right-aligned text using the float's final shrink-to-fit width, preventing overflow clipping from hiding rank text. Handle bottom-edge fallback baselines in middle-aligned atomic boxes so fixed-height inline parents do not vertically displace and clip elements such as Reddit flair emoji. Add a regression test covering the Reddit rank, vote arrows, rendered text, and flair emoji alignment.
This commit is contained in:
@@ -870,8 +870,11 @@ class RichTextInlineLayouter {
|
||||
Float baseline = span.baseline;
|
||||
RichText::BaselineAlignValue baselineAlign = effectiveInlineBaselineAlign(
|
||||
inlineItems, span.inlinePath, span.baselineAlign );
|
||||
// Empty atomic boxes use their bottom edge as the fallback baseline. Include that
|
||||
// exact edge in the middle-alignment path or fixed-height inline parents can clip
|
||||
// the box after it is displaced by a second baseline formula.
|
||||
Float offsetY = baselineAlign.type == RichText::BaselineAlignment::Middle &&
|
||||
baseline > 0.f && baseline < span.size.getHeight()
|
||||
baseline > 0.f && baseline <= span.size.getHeight()
|
||||
? line.maxAscent - baseline
|
||||
: getBaselineAlignedOffset( line, span.size, baseline,
|
||||
span.size.getHeight(),
|
||||
@@ -1053,8 +1056,8 @@ class RichTextInlineLayouter {
|
||||
Float textIndent, Uint32 align, Float forcedLineHeight,
|
||||
const FontStyleConfig& defaultStyle,
|
||||
const std::vector<RichText::FloatExclusion>& externalFloatExclusions,
|
||||
std::vector<RichText::FloatExclusion>& localFloatExclusions,
|
||||
bool lineWrap, RichText::WhiteSpaceWrapMode whiteSpaceWrapMode ) {
|
||||
std::vector<RichText::FloatExclusion>& localFloatExclusions, bool lineWrap,
|
||||
RichText::WhiteSpaceWrapMode whiteSpaceWrapMode ) {
|
||||
LayoutResult result;
|
||||
result.lines.push_back( RichText::RenderParagraph() );
|
||||
|
||||
@@ -1143,8 +1146,7 @@ class RichTextInlineLayouter {
|
||||
for ( ; finalizedLineCount < end; ++finalizedLineCount ) {
|
||||
auto& line = result.lines[finalizedLineCount];
|
||||
alignLineSpans( line, 0.f, defaultStyle, forcedLineHeight, true, inlineItems );
|
||||
finalizedLinesBottom =
|
||||
eemax( finalizedLinesBottom, line.y + line.height );
|
||||
finalizedLinesBottom = eemax( finalizedLinesBottom, line.y + line.height );
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1463,8 +1465,7 @@ class RichTextInlineLayouter {
|
||||
for ( size_t i = externalLeftFloatCount; i < leftFloats.size(); ++i )
|
||||
localFloatExclusions.push_back( { leftFloats[i], RichText::InlineFloat::Left } );
|
||||
for ( size_t i = externalRightFloatCount; i < rightFloats.size(); ++i )
|
||||
localFloatExclusions.push_back(
|
||||
{ rightFloats[i], RichText::InlineFloat::Right } );
|
||||
localFloatExclusions.push_back( { rightFloats[i], RichText::InlineFloat::Right } );
|
||||
|
||||
result.size =
|
||||
Sizef( std::max( maxWidth, floatBoundsRight ), std::max( accumY, floatBoundsBottom ) );
|
||||
|
||||
@@ -376,6 +376,22 @@ void UIHTMLWidget::setCSSPosition( CSSPosition position ) {
|
||||
void UIHTMLWidget::setCSSFloat( CSSFloat cssFloat ) {
|
||||
if ( mFloat != cssFloat ) {
|
||||
mFloat = cssFloat;
|
||||
// A width:auto block normally fills its containing block, while a float uses the CSS
|
||||
// shrink-to-fit width. Represent that used-width distinction with WrapContent so the
|
||||
// floated box's own layouter cannot stretch it back after its parent measured it.
|
||||
if ( mFloat != CSSFloat::None && getLayoutWidthPolicy() == SizePolicy::MatchParent )
|
||||
setLayoutWidthPolicy( SizePolicy::WrapContent );
|
||||
else if ( mFloat == CSSFloat::None && getLayoutWidthPolicy() == SizePolicy::WrapContent &&
|
||||
( mDisplay == CSSDisplay::Block || mDisplay == CSSDisplay::ListItem ) &&
|
||||
( getUIStyle() == nullptr ||
|
||||
getUIStyle()->getProperty( PropertyId::Width ) == nullptr ) &&
|
||||
mPosition != CSSPosition::Absolute && mPosition != CSSPosition::Fixed )
|
||||
setLayoutWidthPolicy( SizePolicy::MatchParent );
|
||||
|
||||
// Float changes the used display type (CSS 2.1 section 9.7), so an inline element must
|
||||
// exchange InlineLayouter for BlockLayouter and vice versa when float is toggled.
|
||||
eeSAFE_DELETE( mLayouter );
|
||||
getLayouter();
|
||||
notifyLayoutAttrChange(
|
||||
toLayoutInvalidationFlags( LayoutInvalidationReason::Style ) |
|
||||
toLayoutInvalidationFlags( LayoutInvalidationReason::FormattingContext ) |
|
||||
|
||||
@@ -36,6 +36,18 @@ UILayouter* UILayouterManager::create( CSSDisplay display, UIWidget* container )
|
||||
return eeNew( BlockLayouter, ( container ) );
|
||||
}
|
||||
|
||||
// CSS 2.1 section 9.7 blockifies floated boxes. Keep flex/grid containers using their
|
||||
// respective formatting contexts, but an ordinary floated inline (for example a <span>)
|
||||
// needs an independent block formatting context instead of InlineLayouter.
|
||||
if ( container->isType( UI_TYPE_HTML_WIDGET ) &&
|
||||
container->asType<UIHTMLWidget>()->getCSSFloat() != CSSFloat::None ) {
|
||||
if ( display == CSSDisplay::Flex || display == CSSDisplay::InlineFlex )
|
||||
return eeNew( FlexLayouter, ( container ) );
|
||||
if ( display == CSSDisplay::Grid || display == CSSDisplay::InlineGrid )
|
||||
return eeNew( GridLayouter, ( container ) );
|
||||
return eeNew( BlockLayouter, ( container ) );
|
||||
}
|
||||
|
||||
switch ( display ) {
|
||||
case CSSDisplay::Block:
|
||||
case CSSDisplay::TableCell:
|
||||
|
||||
@@ -1707,6 +1707,9 @@ void UIRichText::rebuildRichText( UILayout* container, RichText& richText, Intri
|
||||
Float maxWidth = 0;
|
||||
bool isInlineBlockTextSpan =
|
||||
container->isType( UI_TYPE_TEXTSPAN ) && container->asType<UITextSpan>()->isInlineBlock();
|
||||
bool isShrinkToFitFloat = container->isType( UI_TYPE_HTML_WIDGET ) &&
|
||||
container->asType<UIHTMLWidget>()->getCSSFloat() != CSSFloat::None &&
|
||||
container->getLayoutWidthPolicy() == SizePolicy::WrapContent;
|
||||
Node* parentNode = container->getParent();
|
||||
bool parentIsFlexOrGrid = parentNode && parentNode->isType( UI_TYPE_HTML_WIDGET ) &&
|
||||
( parentNode->asType<UIHTMLWidget>()->isFlex() ||
|
||||
@@ -1717,7 +1720,7 @@ void UIRichText::rebuildRichText( UILayout* container, RichText& richText, Intri
|
||||
maxWidth = container->getPixelsSize().getWidth() -
|
||||
container->getPixelsContentOffset().Left -
|
||||
container->getPixelsContentOffset().Right;
|
||||
} else if ( isInlineBlockTextSpan && mode == IntrinsicMode::None &&
|
||||
} else if ( ( isInlineBlockTextSpan || isShrinkToFitFloat ) && mode == IntrinsicMode::None &&
|
||||
container->getLayoutWidthPolicy() == SizePolicy::WrapContent ) {
|
||||
maxWidth = 0;
|
||||
} else if ( parentIsFlexOrGrid &&
|
||||
|
||||
@@ -891,6 +891,92 @@ UTEST( UIHTMLFloat, autoHorizontalMarginsCenterBlockInsideFloat ) {
|
||||
Engine::destroySingleton();
|
||||
}
|
||||
|
||||
UTEST( UIHTMLFloat, autoWidthFloatsShrinkToFitRedditVoteColumn ) {
|
||||
init_float_test();
|
||||
UISceneNode* sceneNode = SceneManager::instance()->getUISceneNode();
|
||||
|
||||
sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( R"html(
|
||||
<body style="margin:0">
|
||||
<div id="link" style="width:800px">
|
||||
<p></p>
|
||||
<span id="rank" style="float:left;margin-top:15px;overflow:hidden;
|
||||
font:16px arial;text-align:right">25</span>
|
||||
<div id="midcol" style="float:left;margin:0 7px;overflow:hidden;
|
||||
font-size:13px;font-weight:bold">
|
||||
<div id="up" style="display:block;width:15px;height:14px;
|
||||
margin:2px auto 0"></div>
|
||||
<div style="text-align:center">•</div>
|
||||
<div id="down" style="display:block;width:15px;height:14px;
|
||||
margin:2px auto 0"></div>
|
||||
</div>
|
||||
</div>
|
||||
<span id="flair" style="display:inline-block;height:16px;line-height:16px;
|
||||
overflow:hidden;padding:0 4px">
|
||||
<span>Humor </span><span id="emoji" style="display:inline-block;width:15px;
|
||||
height:15px;vertical-align:middle"></span>
|
||||
</span>
|
||||
</body>
|
||||
)html" ) );
|
||||
sceneNode->updateDirtyLayouts();
|
||||
|
||||
auto* link = sceneNode->find<UIWidget>( "link" );
|
||||
auto* rank = sceneNode->find<UIWidget>( "rank" );
|
||||
auto* midcol = sceneNode->find<UIWidget>( "midcol" );
|
||||
auto* up = sceneNode->find<UIWidget>( "up" );
|
||||
auto* down = sceneNode->find<UIWidget>( "down" );
|
||||
auto* flair = sceneNode->find<UIWidget>( "flair" );
|
||||
auto* emoji = sceneNode->find<UIWidget>( "emoji" );
|
||||
ASSERT_TRUE( link != nullptr );
|
||||
ASSERT_TRUE( rank != nullptr );
|
||||
ASSERT_TRUE( midcol != nullptr );
|
||||
ASSERT_TRUE( up != nullptr );
|
||||
ASSERT_TRUE( down != nullptr );
|
||||
ASSERT_TRUE( flair != nullptr );
|
||||
ASSERT_TRUE( emoji != nullptr );
|
||||
|
||||
const Vector2f linkPos = link->convertToWorldSpace( { 0, 0 } );
|
||||
const Vector2f rankPos = rank->convertToWorldSpace( { 0, 0 } );
|
||||
const Vector2f midcolPos = midcol->convertToWorldSpace( { 0, 0 } );
|
||||
const Vector2f upPos = up->convertToWorldSpace( { 0, 0 } );
|
||||
const Vector2f downPos = down->convertToWorldSpace( { 0, 0 } );
|
||||
|
||||
EXPECT_GT( rank->getPixelsSize().getWidth(), 0.f );
|
||||
EXPECT_GT( rank->asType<UIRichText>()->getRichTextPtr()->getSize().getWidth(), 0.f );
|
||||
EXPECT_FALSE( rank->asType<UIRichText>()->getRichText().getLines().empty() );
|
||||
EXPECT_LT( rank->getPixelsSize().getWidth(), 40.f );
|
||||
EXPECT_LT( midcol->getPixelsSize().getWidth(), 40.f );
|
||||
EXPECT_NEAR( rankPos.x, linkPos.x, 1.f );
|
||||
EXPECT_NEAR( midcolPos.x, rankPos.x + rank->getPixelsSize().getWidth() + 7.f, 1.f );
|
||||
EXPECT_NEAR( upPos.x + up->getPixelsSize().getWidth() / 2.f,
|
||||
midcolPos.x + midcol->getPixelsSize().getWidth() / 2.f, 1.f );
|
||||
EXPECT_NEAR( downPos.x, upPos.x, 1.f );
|
||||
const Vector2f flairPos = flair->convertToWorldSpace( { 0, 0 } );
|
||||
const Vector2f emojiPos = emoji->convertToWorldSpace( { 0, 0 } );
|
||||
EXPECT_GE( emojiPos.y, flairPos.y - 0.01f );
|
||||
EXPECT_LE( emojiPos.y + emoji->getPixelsSize().getHeight(),
|
||||
flairPos.y + flair->getPixelsSize().getHeight() + 0.01f );
|
||||
|
||||
auto* window = Engine::instance()->getCurrentWindow();
|
||||
window->setClearColor( Color::White );
|
||||
window->clear();
|
||||
SceneManager::instance()->draw();
|
||||
window->display();
|
||||
Image framebuffer = window->getFrontBufferImage();
|
||||
bool rankPainted = false;
|
||||
for ( int y = (int)rankPos.y; y < (int)( rankPos.y + rank->getPixelsSize().getHeight() );
|
||||
++y ) {
|
||||
for ( int x = (int)rankPos.x; x < (int)( rankPos.x + rank->getPixelsSize().getWidth() );
|
||||
++x ) {
|
||||
Color pixel = framebuffer.getPixel( x, y );
|
||||
if ( pixel.r < 245 || pixel.g < 245 || pixel.b < 245 )
|
||||
rankPainted = true;
|
||||
}
|
||||
}
|
||||
EXPECT_TRUE( rankPainted );
|
||||
|
||||
Engine::destroySingleton();
|
||||
}
|
||||
|
||||
UTEST( UIHTMLFloat, rightFloatConstrainsTextInsideFollowingNormalBlock ) {
|
||||
init_float_test();
|
||||
UISceneNode* sceneNode = SceneManager::instance()->getUISceneNode();
|
||||
|
||||
Reference in New Issue
Block a user