mirror of
https://github.com/SpartanJ/eepp.git
synced 2026-10-01 18:50:16 +03:00
Fix nested float propagation in rich text layout
Propagate descendant float exclusions through non-BFC HTML containers so floats participate in their nearest block formatting context. Finalize line geometry before translating propagated exclusions, track finalized lines incrementally, and search successive float bands when a fixed-width inline-block does not fit beside active floats. Recognize inline-block elements as block formatting contexts while preserving their fixed width. Reuse persistent float-exclusion storage across relayouts to avoid repeated allocations. Add SS64 regressions covering narrow viewports, HiDPI rendering, stacked floats, anchor sizing, SVG sizing, and footer overlap. Fixed `cursor: pointer`, `pointer` was not registered.
This commit is contained in:
+107
-18
@@ -737,7 +737,7 @@ class RichTextInlineLayouter {
|
||||
for ( const auto& run : buildLayoutRuns( inlineItems ) ) {
|
||||
const auto& payload = run.payload;
|
||||
if ( payload.type != RichText::RenderSpan::Type::Text ) {
|
||||
if ( payload.floatType != RichText::InlineFloat::None ||
|
||||
if ( payload.floatType != RichText::InlineFloat::None || payload.propagatedFloats ||
|
||||
payload.clearType != RichText::InlineClear::None )
|
||||
return true;
|
||||
}
|
||||
@@ -1053,6 +1053,7 @@ 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 ) {
|
||||
LayoutResult result;
|
||||
result.lines.push_back( RichText::RenderParagraph() );
|
||||
@@ -1134,6 +1135,27 @@ class RichTextInlineLayouter {
|
||||
return advanced;
|
||||
};
|
||||
|
||||
size_t finalizedLineCount = 0;
|
||||
Float finalizedLinesBottom = 0.f;
|
||||
// Propagated float groups need final line metrics before translating their exclusions.
|
||||
// Keep a monotonic cursor so each preceding line is finalized at most once here.
|
||||
auto finalizeLinesThrough = [&]( size_t end ) {
|
||||
for ( ; finalizedLineCount < end; ++finalizedLineCount ) {
|
||||
auto& line = result.lines[finalizedLineCount];
|
||||
alignLineSpans( line, 0.f, defaultStyle, forcedLineHeight, true, inlineItems );
|
||||
finalizedLinesBottom =
|
||||
eemax( finalizedLinesBottom, line.y + line.height );
|
||||
}
|
||||
};
|
||||
|
||||
auto advanceInFlowLine = [&]() {
|
||||
finalizeLinesThrough( result.lines.size() );
|
||||
curY = eemax( curY, finalizedLinesBottom );
|
||||
result.lines.push_back( RichText::RenderParagraph() );
|
||||
result.lines.back().y = curY;
|
||||
curX = 0;
|
||||
};
|
||||
|
||||
for ( const auto& run : runs ) {
|
||||
const auto& payload = run.payload;
|
||||
if ( payload.type == RichText::RenderSpan::Type::Text ) {
|
||||
@@ -1189,8 +1211,7 @@ class RichTextInlineLayouter {
|
||||
inlineEndSpacing( payload, inlineItems ) );
|
||||
if ( lineWrap && effW > 0 && effW < 1e9f && curX > effW ) {
|
||||
maxWidth = std::max( maxWidth, curX );
|
||||
result.lines.push_back( RichText::RenderParagraph() );
|
||||
curX = 0;
|
||||
advanceInFlowLine();
|
||||
continue;
|
||||
}
|
||||
}
|
||||
@@ -1209,8 +1230,7 @@ class RichTextInlineLayouter {
|
||||
}
|
||||
if ( !trailingNewlineAlreadyAdvanced ) {
|
||||
maxWidth = std::max( maxWidth, curX );
|
||||
result.lines.push_back( RichText::RenderParagraph() );
|
||||
curX = 0;
|
||||
advanceInFlowLine();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1313,7 +1333,8 @@ class RichTextInlineLayouter {
|
||||
Float re = floatRightEdge( curY );
|
||||
Float availableWidth = re - le;
|
||||
if ( availableWidth > 0 && availableWidth < 1e9f ) {
|
||||
if ( metrics.size.getWidth() > availableWidth + 0.01f )
|
||||
if ( metrics.isBlock &&
|
||||
metrics.size.getWidth() > availableWidth + 0.01f )
|
||||
metrics.size.setWidth( availableWidth );
|
||||
curX = le;
|
||||
effW = availableWidth;
|
||||
@@ -1322,16 +1343,24 @@ class RichTextInlineLayouter {
|
||||
|
||||
if ( lineWrap && !metrics.isBlock && effW > 0 && effW < 1e9f &&
|
||||
metrics.size.getWidth() > effW + 0.01f ) {
|
||||
Float maxBottom = activeFloatBottom( curY );
|
||||
if ( maxBottom > curY ) {
|
||||
Float placedY = curY;
|
||||
Float placedWidth = effW;
|
||||
while ( metrics.size.getWidth() > placedWidth + 0.01f ) {
|
||||
Float nextY = activeFloatBottom( placedY );
|
||||
if ( nextY <= placedY )
|
||||
break;
|
||||
placedY = nextY;
|
||||
placedWidth = effectiveMaxWidthAt( placedY );
|
||||
}
|
||||
if ( placedY > curY ) {
|
||||
maxWidth = std::max( maxWidth, curX );
|
||||
if ( !result.lines.back().spans.empty() )
|
||||
result.lines.push_back( RichText::RenderParagraph() );
|
||||
curX = 0;
|
||||
curY = maxBottom;
|
||||
curY = placedY;
|
||||
result.lines.back().y = curY;
|
||||
le = floatLeftEdge( curY );
|
||||
effW = effectiveMaxWidthAt( curY );
|
||||
effW = placedWidth;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1339,20 +1368,60 @@ class RichTextInlineLayouter {
|
||||
( curX + metrics.size.getWidth() >= effW || curX >= effW ) && curX > 0 &&
|
||||
hadLineContentBeforeSpacing ) {
|
||||
maxWidth = std::max( maxWidth, curX );
|
||||
result.lines.push_back( RichText::RenderParagraph() );
|
||||
curX = 0;
|
||||
advanceInFlowLine();
|
||||
if ( hadLineContentBeforeSpacing )
|
||||
addInlineSpacingToCurrentLine( result, curX, startSpacing );
|
||||
}
|
||||
|
||||
if ( payload.propagatedFloats ) {
|
||||
finalizeLinesThrough( result.lines.size() - 1 );
|
||||
if ( finalizedLinesBottom > curY ) {
|
||||
curY = finalizedLinesBottom;
|
||||
result.lines.back().y = curY;
|
||||
}
|
||||
Float placedY = curY;
|
||||
bool moved;
|
||||
do {
|
||||
moved = false;
|
||||
for ( const auto& propagated : *payload.propagatedFloats ) {
|
||||
Rectf rect = propagated.rect;
|
||||
rect.move( { curX, placedY } );
|
||||
auto avoidActiveFloat = [&]( const Rectf& active ) {
|
||||
if ( rect.intersect( active ) && active.Bottom > placedY ) {
|
||||
placedY += active.Bottom - rect.Top;
|
||||
moved = true;
|
||||
}
|
||||
};
|
||||
for ( const auto& active : leftFloats )
|
||||
avoidActiveFloat( active );
|
||||
for ( const auto& active : rightFloats )
|
||||
avoidActiveFloat( active );
|
||||
}
|
||||
} while ( moved );
|
||||
if ( placedY > curY ) {
|
||||
curY = placedY;
|
||||
result.lines.back().y = curY;
|
||||
}
|
||||
}
|
||||
|
||||
appendAtomicRenderSpan( result.lines.back(), payload, metrics, curX,
|
||||
curCharIdx );
|
||||
if ( payload.propagatedFloats ) {
|
||||
const Vector2f atomicPosition = result.lines.back().spans.back().position;
|
||||
for ( const auto& propagated : *payload.propagatedFloats ) {
|
||||
Rectf rect = propagated.rect;
|
||||
rect.move( { atomicPosition.x, curY } );
|
||||
if ( propagated.type == RichText::InlineFloat::Left )
|
||||
leftFloats.push_back( rect );
|
||||
else if ( propagated.type == RichText::InlineFloat::Right )
|
||||
rightFloats.push_back( rect );
|
||||
}
|
||||
}
|
||||
addInlineSpacingToCurrentLine( result, curX, endSpacing );
|
||||
|
||||
if ( lineWrap && effW > 0 && effW < 1e9f && curX >= effW ) {
|
||||
maxWidth = std::max( maxWidth, curX );
|
||||
result.lines.push_back( RichText::RenderParagraph() );
|
||||
curX = 0;
|
||||
advanceInFlowLine();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1386,6 +1455,16 @@ class RichTextInlineLayouter {
|
||||
floatBoundsBottom = std::max( floatBoundsBottom, rightFloats[i].Bottom );
|
||||
floatBoundsRight = std::max( floatBoundsRight, rightFloats[i].Right );
|
||||
}
|
||||
// This is persistent RichText storage: clear without discarding capacity so repeated
|
||||
// viewport-driven layouts do not allocate a fresh exclusion vector every time.
|
||||
localFloatExclusions.clear();
|
||||
localFloatExclusions.reserve( leftFloats.size() + rightFloats.size() -
|
||||
externalLeftFloatCount - externalRightFloatCount );
|
||||
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 } );
|
||||
|
||||
result.size =
|
||||
Sizef( std::max( maxWidth, floatBoundsRight ), std::max( accumY, floatBoundsBottom ) );
|
||||
@@ -1638,6 +1717,7 @@ class RichTextInlineLayouter {
|
||||
run.payload.isLineBreak = box.isLineBreak;
|
||||
run.payload.isBlock = box.isBlock;
|
||||
run.payload.isBlockFormattingContext = box.isBlockFormattingContext;
|
||||
run.payload.propagatedFloats = box.propagatedFloats;
|
||||
run.payload.baselineAlign = box.baselineAlign;
|
||||
run.payload.inlinePath = path;
|
||||
run.payload._leafIndex = nextLeafIndex++;
|
||||
@@ -2116,9 +2196,11 @@ void RichText::addDrawable( std::shared_ptr<Drawable> drawable ) {
|
||||
invalidateLayout();
|
||||
}
|
||||
|
||||
void RichText::addCustomSize( const Sizef& size, InlineFloat floatType, InlineClear clearType,
|
||||
Float baseline, const BaselineAlignValue& baselineAlign,
|
||||
InlineSource source, bool isBlock, bool isBlockFormattingContext ) {
|
||||
void RichText::addCustomSize(
|
||||
const Sizef& size, InlineFloat floatType, InlineClear clearType, Float baseline,
|
||||
const BaselineAlignValue& baselineAlign, InlineSource source, bool isBlock,
|
||||
bool isBlockFormattingContext,
|
||||
std::shared_ptr<const std::vector<FloatExclusion>> propagatedFloats ) {
|
||||
Float usedBaseline = baseline >= 0.f ? baseline : size.getHeight();
|
||||
|
||||
InlineItem item;
|
||||
@@ -2130,6 +2212,7 @@ void RichText::addCustomSize( const Sizef& size, InlineFloat floatType, InlineCl
|
||||
box.clearType = clearType;
|
||||
box.isBlock = isBlock;
|
||||
box.isBlockFormattingContext = isBlockFormattingContext;
|
||||
box.propagatedFloats = std::move( propagatedFloats );
|
||||
box.baselineAlign = baselineAlign;
|
||||
item.data = std::move( box );
|
||||
resolveInlinePath( mInlineItems, mInlinePath )->push_back( std::move( item ) );
|
||||
@@ -2359,6 +2442,7 @@ void RichText::updateLayout() {
|
||||
mAlign, mLineHeight, mDefaultStyle,
|
||||
mLineWrap, mWhiteSpaceWrapMode );
|
||||
mLines = std::move( result.lines );
|
||||
mLocalFloatExclusions.clear();
|
||||
mSize = result.size;
|
||||
mTotalCharacterCount = result.totalCharacterCount;
|
||||
rebuildInlineFragments();
|
||||
@@ -2368,7 +2452,7 @@ void RichText::updateLayout() {
|
||||
|
||||
auto result = RichTextInlineLayouter::layoutWithFloats(
|
||||
mInlineItems, mMaxWidth, mTextIndent, mAlign, mLineHeight, mDefaultStyle,
|
||||
mExternalFloatExclusions, mLineWrap, mWhiteSpaceWrapMode );
|
||||
mExternalFloatExclusions, mLocalFloatExclusions, mLineWrap, mWhiteSpaceWrapMode );
|
||||
mLines = std::move( result.lines );
|
||||
mSize = result.size;
|
||||
mTotalCharacterCount = result.totalCharacterCount;
|
||||
@@ -2376,6 +2460,11 @@ void RichText::updateLayout() {
|
||||
mNeedsLayoutUpdate = false;
|
||||
}
|
||||
|
||||
const std::vector<RichText::FloatExclusion>& RichText::getLocalFloatExclusions() {
|
||||
updateLayout();
|
||||
return mLocalFloatExclusions;
|
||||
}
|
||||
|
||||
Sizef RichText::getSize() {
|
||||
updateLayout();
|
||||
return mSize;
|
||||
|
||||
@@ -2116,7 +2116,7 @@ void UIRichText::rebuildRichText( UILayout* container, RichText& richText, Intri
|
||||
bool isFloating = floatType != CSSFloat::None;
|
||||
bool isNormalFlowBlock = isBlock && !isFloating;
|
||||
bool isBlockFormattingContext =
|
||||
isNormalFlowBlock && widget->isType( UI_TYPE_HTML_WIDGET ) &&
|
||||
!isFloating && widget->isType( UI_TYPE_HTML_WIDGET ) &&
|
||||
widget->asType<UIHTMLWidget>()->establishesBlockFormattingContext();
|
||||
bool shrinkToFitFloat =
|
||||
isFloating && widget->getLayoutWidthPolicy() == SizePolicy::MatchParent;
|
||||
@@ -2145,11 +2145,30 @@ void UIRichText::rebuildRichText( UILayout* container, RichText& richText, Intri
|
||||
|
||||
Sizef customSize( w + margin.Left + margin.Right,
|
||||
size.getHeight() + margin.Top + margin.Bottom );
|
||||
richText.addCustomSize(
|
||||
customSize, toRichTextFloat( floatType ), toRichTextClear( clearType ),
|
||||
getAtomicInlineBoxBaseline( widget, size, margin ),
|
||||
toRichTextBaselineAlign( getWidgetBaselineAlign( widget ) ),
|
||||
toRichTextWidgetSource( widget ), isNormalFlowBlock, isBlockFormattingContext );
|
||||
std::shared_ptr<std::vector<RichText::FloatExclusion>> propagatedFloats;
|
||||
if ( widget->isType( UI_TYPE_HTML_WIDGET ) ) {
|
||||
auto* htmlWidget = widget->asType<UIHTMLWidget>();
|
||||
if ( !htmlWidget->establishesBlockFormattingContext() &&
|
||||
htmlWidget->getRichTextPtr() ) {
|
||||
const auto& localFloats =
|
||||
htmlWidget->getRichTextPtr()->getLocalFloatExclusions();
|
||||
if ( !localFloats.empty() ) {
|
||||
propagatedFloats =
|
||||
std::make_shared<std::vector<RichText::FloatExclusion>>(
|
||||
localFloats );
|
||||
const Rectf contentOffset = widget->getPixelsContentOffset();
|
||||
for ( auto& exclusion : *propagatedFloats )
|
||||
exclusion.rect.move( { margin.Left + contentOffset.Left,
|
||||
margin.Top + contentOffset.Top } );
|
||||
}
|
||||
}
|
||||
}
|
||||
richText.addCustomSize( customSize, toRichTextFloat( floatType ),
|
||||
toRichTextClear( clearType ),
|
||||
getAtomicInlineBoxBaseline( widget, size, margin ),
|
||||
toRichTextBaselineAlign( getWidgetBaselineAlign( widget ) ),
|
||||
toRichTextWidgetSource( widget ), isNormalFlowBlock,
|
||||
isBlockFormattingContext, std::move( propagatedFloats ) );
|
||||
|
||||
if ( widget->isType( UI_TYPE_TEXTSPAN ) &&
|
||||
widget->asType<UITextSpan>()->isInlineBlock() &&
|
||||
|
||||
@@ -63,7 +63,7 @@ Cursor::Type Cursor::fromName( std::string name ) {
|
||||
String::toLowerInPlace( name );
|
||||
if ( "arrow" == name )
|
||||
return Arrow;
|
||||
if ( "hand" == name )
|
||||
if ( "hand" == name || "pointer" == name )
|
||||
return Hand;
|
||||
if ( "ibream" == name )
|
||||
return IBeam;
|
||||
|
||||
@@ -618,14 +618,21 @@ UTEST( UIHTMLFloat, floatedListItemsShrinkToFitBlockAnchors ) {
|
||||
UTEST( UIHTMLFloat, ss64BlockAnchorsAndSvgAtPixelDensity2 ) {
|
||||
init_float_test();
|
||||
PixelDensity::setPixelDensity( 2.f );
|
||||
Engine::instance()->getCurrentWindow()->setSize( 475, 900 );
|
||||
UISceneNode* sceneNode = SceneManager::instance()->getUISceneNode();
|
||||
std::string html;
|
||||
ASSERT_TRUE( FileSystem::fileGet( "assets/html/ss64.html", html ) );
|
||||
sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( html ) );
|
||||
sceneNode->updateDirtyLayouts();
|
||||
SceneManager::instance()->update();
|
||||
|
||||
auto buttons = sceneNode->getRoot()->querySelectorAll( ".tbtn" );
|
||||
auto anchors = sceneNode->getRoot()->querySelectorAll( ".tbtn a" );
|
||||
auto worldRect = []( UIWidget* widget ) {
|
||||
Vector2f position = widget->getPixelsPosition();
|
||||
widget->nodeToWorldTranslation( position );
|
||||
return Rectf( position, widget->getPixelsSize() );
|
||||
};
|
||||
ASSERT_EQ( buttons.size(), (size_t)8 );
|
||||
ASSERT_EQ( anchors.size(), buttons.size() );
|
||||
for ( size_t i = 0; i < buttons.size(); ++i ) {
|
||||
@@ -634,6 +641,8 @@ UTEST( UIHTMLFloat, ss64BlockAnchorsAndSvgAtPixelDensity2 ) {
|
||||
EXPECT_NEAR( anchor->getPixelsSize().getHeight(), 80.f, 1.f );
|
||||
EXPECT_NEAR( anchor->getPixelsSize().getHeight() + 4.f,
|
||||
buttons[i]->getPixelsSize().getHeight(), 1.f );
|
||||
for ( size_t j = 0; j < i; ++j )
|
||||
EXPECT_FALSE( worldRect( buttons[i] ).intersect( worldRect( buttons[j] ) ) );
|
||||
}
|
||||
auto* svg = sceneNode->getRoot()->querySelector( "#sherlock svg" )->asType<UISvg>();
|
||||
auto* input = sceneNode->getRoot()->querySelector( "#qu" );
|
||||
@@ -656,10 +665,42 @@ UTEST( UIHTMLFloat, ss64BlockAnchorsAndSvgAtPixelDensity2 ) {
|
||||
1.f );
|
||||
EXPECT_NEAR( svg->getDrawable()->getPixelsSize().getHeight(), svg->getPixelsSize().getHeight(),
|
||||
1.f );
|
||||
auto* footer = sceneNode->getRoot()->querySelector( ".footer" )->asType<UIRichText>();
|
||||
ASSERT_TRUE( footer != nullptr );
|
||||
for ( auto* button : buttons ) {
|
||||
EXPECT_FALSE( worldRect( footer ).intersect( worldRect( button ) ) );
|
||||
}
|
||||
Engine::destroySingleton();
|
||||
PixelDensity::setPixelDensity( 1.f );
|
||||
}
|
||||
|
||||
UTEST( UIHTMLFloat, ss64NarrowViewportFloatsDoNotOverlapAtPixelDensity1 ) {
|
||||
init_float_test();
|
||||
auto* window = Engine::instance()->getCurrentWindow();
|
||||
ASSERT_TRUE( window != nullptr );
|
||||
window->setSize( 400, 600 );
|
||||
UISceneNode* sceneNode = SceneManager::instance()->getUISceneNode();
|
||||
std::string html;
|
||||
ASSERT_TRUE( FileSystem::fileGet( "assets/html/ss64.html", html ) );
|
||||
sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( html ) );
|
||||
sceneNode->updateDirtyLayouts();
|
||||
SceneManager::instance()->update();
|
||||
|
||||
auto buttons = sceneNode->getRoot()->querySelectorAll( ".tbtn" );
|
||||
ASSERT_EQ( buttons.size(), (size_t)8 );
|
||||
auto worldRect = []( UIWidget* widget ) {
|
||||
Vector2f position = widget->getPixelsPosition();
|
||||
widget->nodeToWorldTranslation( position );
|
||||
return Rectf( position, widget->getPixelsSize() );
|
||||
};
|
||||
for ( size_t i = 0; i < buttons.size(); ++i ) {
|
||||
for ( size_t j = 0; j < i; ++j )
|
||||
EXPECT_FALSE( worldRect( buttons[i] ).intersect( worldRect( buttons[j] ) ) );
|
||||
}
|
||||
|
||||
Engine::destroySingleton();
|
||||
}
|
||||
|
||||
UTEST( UIHTMLFloat, autoHorizontalMarginsCenterBlockInsideFloat ) {
|
||||
init_float_test();
|
||||
UISceneNode* sceneNode = SceneManager::instance()->getUISceneNode();
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
#include <eepp/ui/uiscenenode.hpp>
|
||||
#include <eepp/ui/uithememanager.hpp>
|
||||
#include <eepp/ui/uiwidget.hpp>
|
||||
#include <eepp/window/cursor.hpp>
|
||||
#include <eepp/window/engine.hpp>
|
||||
#include <eepp/window/input.hpp>
|
||||
|
||||
@@ -19,6 +20,11 @@ using namespace EE::Window;
|
||||
using namespace EE::Scene;
|
||||
using namespace EE::UI;
|
||||
|
||||
UTEST( UISceneNode, CssPointerCursorUsesHandCursor ) {
|
||||
EXPECT_EQ( Cursor::fromName( "pointer" ), Cursor::Hand );
|
||||
EXPECT_EQ( Cursor::fromName( "POINTER" ), Cursor::Hand );
|
||||
}
|
||||
|
||||
static UISceneNode* init_test_scene_node() {
|
||||
FileSystem::changeWorkingDirectory( Sys::getProcessPath() );
|
||||
FontTrueType* font = FontTrueType::New( "NotoSans-Regular" ).get();
|
||||
|
||||
Reference in New Issue
Block a user