From 852adaa2c546883b95821095c8198e3fe16ece44 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=ADn=20Lucas=20Golini?= Date: Fri, 15 May 2026 02:42:05 -0300 Subject: [PATCH] CSS Block Semantics plan executed. --- .../eepp-ui-anchor-padding-lineheight.webp | Bin 1248 -> 1246 bytes .../assets/html/eepp-ui-anchor-padding.webp | Bin 1248 -> 1248 bytes .../html/eepp-uihtmltable-complex-layout.webp | Bin 13458 -> 13456 bytes include/eepp/graphics/richtext.hpp | 12 +- include/eepp/ui/uihtmltextarea.hpp | 2 + include/eepp/ui/uitextspan.hpp | 2 - include/eepp/ui/uiwidget.hpp | 2 +- src/eepp/graphics/richtext.cpp | 191 +++++++----------- src/eepp/ui/blocklayouter.cpp | 21 +- src/eepp/ui/uihtmlinput.cpp | 1 + src/eepp/ui/uihtmltextarea.cpp | 4 + src/eepp/ui/uirichtext.cpp | 46 ++++- src/eepp/ui/uitextspan.cpp | 27 +-- src/tests/unit_tests/richtext_tests.cpp | 17 ++ src/tests/unit_tests/uihtml_tests.cpp | 34 ++++ 15 files changed, 182 insertions(+), 177 deletions(-) diff --git a/bin/unit_tests/assets/html/eepp-ui-anchor-padding-lineheight.webp b/bin/unit_tests/assets/html/eepp-ui-anchor-padding-lineheight.webp index 5cdf1db375c93dbcfce35681d65f8a8d6d7dca4f..2413114d9014ef0d1c30bc2acfbf72a6be471299 100644 GIT binary patch delta 325 zcmV-L0lNO+3El}2Qb|Te)&u|mS4BclR!}%h$*~a~1AmkM#Ie&qV%H2mUit68_u5yj zQ9avNX#YjpyLP~Z3RyiIkgNt0RIE`U2BniDV{0E2%#OzSDmBRdfdmz6RER;sidE|7 z1QOS%R-m1O7q2Z!rAMZc$ejm8%A`g8NVLy@L`^?XDm^llM4WiI*bFF}8R^0T(J{^>9G48c0yFMuixZPL7PNeNZqv8t1FjAo~XrRIE`U1_>)xshbl> zT%%fnb`D;=wkVYznMxve9uz5)7WE_1J_8ap{XnVo$W#(>;^AU5ploKOYwzIsv_fky z__RjSa(|cVr7d)>F3>(c(EbChz2MUtNs3pk)E;Q#4KEijMP;<`g>MVQZj!dJcf)M? zf%f=V3t#xQK%=aIENP(D3U zId@fSR!$F`EnB@@G?1gE7jm@o{j{@XtCx#Lt{8@u^40+o*RGPkv19iGl>Zd3AFp%v l_g0e;10R!I1PKKXK99#(lb{4Sldl9Sldl8^1^>Uj0RVlEnO*<@ diff --git a/bin/unit_tests/assets/html/eepp-ui-anchor-padding.webp b/bin/unit_tests/assets/html/eepp-ui-anchor-padding.webp index bbbb9fc64c0cdbbb203b1f0cd2154cd777fc85ec..e127945eb1ef7a5f13ef4a6150c7ffe2fd6eed13 100644 GIT binary patch delta 480 zcmaFB`G9i+H*@```Rq4m&fl2bHh=ve@yC1lu4Y|3@sHQ4W-8x0*@BHpX8i0b>pNSf zW`+4Q7->#e*~`!M)M4_UsnXAWcpUOF)mC#@y_^CEa=2! zao5w!oSQvLPIMeOYZqk2Y&6|zPQ{KZG8)raUs_vK*sZ;IjN2}ELj9UQPk+7Sn!ldC zq=UcYhljNOk1dXugsy6pGu)~=eEO(&B&%-i!`ng|XD`!!bXTLAaRIa4)l1_0bT3+n zJ}P6lRrzpJvv{brY*)JLwMh;eKV=3kN}6?Ed!A^d_oAb$DX$qqPBdIO7rQko)QI8v zsjC|DyianTw$6E5YyUoGr_|HM^%HjoKCLNjR4H90mhtGuU!JG-dtax&neq4V)c;TR c|8K6eIX3D4)B2zB|EAM~ilBwr$;Y@D0M6|k9{>OV delta 480 zcmaFB`G9i+H*@{}K#R|xOV5AUJoSH4-Nl7bXQgiLcWhsO>EKpoy|6RS9yUBNowy(F*pspU?0 z_#CYDW6C}(%3ALHam|S+A&oFQU9S+I&y`0vD`k6b+0l?A+rT9`;nvR8VXM8^9>naF zdb+rA&e}Z_Ht&7^e6Du*;u@2pdh=Ct*6U4J;&+wFS7!J81%H-T{H>Y0xqkbn_#Ouv Zx#W}oKkfgc|Nk_ts0dn^Eg$T~002giBIy7C diff --git a/bin/unit_tests/assets/html/eepp-uihtmltable-complex-layout.webp b/bin/unit_tests/assets/html/eepp-uihtmltable-complex-layout.webp index cf3e3084dc248c2c344b0cfcb719334d13b68ae3..41e5aa69c0b54405ace96e60ffc6ca540f7892e6 100644 GIT binary patch delta 2144 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 diff --git a/include/eepp/graphics/richtext.hpp b/include/eepp/graphics/richtext.hpp index 6e340e504..3185e52e1 100644 --- a/include/eepp/graphics/richtext.hpp +++ b/include/eepp/graphics/richtext.hpp @@ -35,7 +35,7 @@ class EE_API RichText : public Drawable { void addSpan( const String& text, const FontStyleConfig& style ); void addSpan( const String& text, const FontStyleConfig& style, const Rectf& margin, - const Rectf& padding, Float lineHeight = 0, bool isAtomic = false ); + const Rectf& padding, Float lineHeight = 0 ); /** * @brief Adds a text span with individual style parameters. @@ -83,9 +83,9 @@ class EE_API RichText : public Drawable { struct CustomBlock { Sizef size; - bool isBlock{ false }; UI::CSSFloat floatType{ UI::CSSFloat::None }; UI::CSSClear clearType{ UI::CSSClear::None }; + bool isLineBreak{ false }; }; struct SpanBlock { @@ -93,7 +93,6 @@ class EE_API RichText : public Drawable { Rectf margin; Rectf padding; Float lineHeight{ 0 }; - bool isAtomic{ false }; }; using Block = std::variant, CustomBlock>; @@ -107,12 +106,13 @@ class EE_API RichText : public Drawable { /** * @brief Adds a custom size spacer into the text flow. * @param size The physical dimensions of the spacer. - * @param isBlock Whether this spacer acts as a block-level element. */ - void addCustomSize( const Sizef& size, bool isBlock = false, - UI::CSSFloat floatType = UI::CSSFloat::None, + void addCustomSize( const Sizef& size, UI::CSSFloat floatType = UI::CSSFloat::None, UI::CSSClear clearType = UI::CSSClear::None ); + /** @brief Adds a virtual line break that is not associated with a DOM text character. */ + void addLineBreak(); + /** @return The list of blocks. */ const std::vector& getBlocks() { return mBlocks; } diff --git a/include/eepp/ui/uihtmltextarea.hpp b/include/eepp/ui/uihtmltextarea.hpp index a14bfb2ff..b7487de19 100644 --- a/include/eepp/ui/uihtmltextarea.hpp +++ b/include/eepp/ui/uihtmltextarea.hpp @@ -15,6 +15,8 @@ class EE_API UIHTMLTextArea : public UITextEdit { virtual bool isType( const Uint32& type ) const; + virtual bool isInlineDisplay() const; + virtual bool applyProperty( const StyleSheetProperty& attribute ); virtual std::string getPropertyString( const PropertyDefinition* propertyDef, diff --git a/include/eepp/ui/uitextspan.hpp b/include/eepp/ui/uitextspan.hpp index 0bc02f180..1cb9a1e11 100644 --- a/include/eepp/ui/uitextspan.hpp +++ b/include/eepp/ui/uitextspan.hpp @@ -150,8 +150,6 @@ class EE_API UITextSpan : public UIRichText { explicit UITextSpan( const std::string& tag = "span" ); - virtual void drawBorder(); - virtual void onTextChanged(); virtual void onFontChanged(); diff --git a/include/eepp/ui/uiwidget.hpp b/include/eepp/ui/uiwidget.hpp index 29870a845..b2ddb9e10 100644 --- a/include/eepp/ui/uiwidget.hpp +++ b/include/eepp/ui/uiwidget.hpp @@ -812,7 +812,7 @@ class EE_API UIWidget : public UINode { * * @return True if this widget is inline-level. */ - bool isInlineDisplay() const; + virtual bool isInlineDisplay() const; /** @return The index of this element among its sibling elements. */ Uint32 getElementIndex() const; diff --git a/src/eepp/graphics/richtext.cpp b/src/eepp/graphics/richtext.cpp index 85725ec64..24f238ebd 100644 --- a/src/eepp/graphics/richtext.cpp +++ b/src/eepp/graphics/richtext.cpp @@ -302,14 +302,14 @@ Sizef RichText::getPixelsSize() { } void RichText::addSpan( const String& text, const FontStyleConfig& style, const Rectf& margin, - const Rectf& padding, Float lineHeight, bool isAtomic ) { + const Rectf& padding, Float lineHeight ) { if ( text.empty() && margin == Rectf::Zero && padding == Rectf::Zero && lineHeight == 0 ) return; auto span = std::make_shared(); span->setString( text ); span->setStyleConfig( style ); - mBlocks.push_back( SpanBlock{ span, margin, padding, lineHeight, isAtomic } ); + mBlocks.push_back( SpanBlock{ span, margin, padding, lineHeight } ); invalidateLayout(); } @@ -320,9 +320,13 @@ void RichText::addDrawable( std::shared_ptr drawable ) { invalidateLayout(); } -void RichText::addCustomSize( const Sizef& size, bool isBlock, UI::CSSFloat floatType, - UI::CSSClear clearType ) { - mBlocks.push_back( CustomBlock{ size, isBlock, floatType, clearType } ); +void RichText::addCustomSize( const Sizef& size, UI::CSSFloat floatType, UI::CSSClear clearType ) { + mBlocks.push_back( CustomBlock{ size, floatType, clearType, false } ); + invalidateLayout(); +} + +void RichText::addLineBreak() { + mBlocks.push_back( CustomBlock{ Sizef::Zero, UI::CSSFloat::None, UI::CSSClear::None, true } ); invalidateLayout(); } @@ -443,15 +447,8 @@ Float RichText::getMaxIntrinsicWidth() { } else if ( auto pDrawable = std::get_if>( &block ) ) { curX += ( *pDrawable )->getPixelsSize().getWidth(); } else if ( auto pSize = std::get_if( &block ) ) { - if ( pSize->isBlock ) { - if ( curX > 0 ) { - maxW = std::max( maxW, curX ); - curX = 0; - } - maxW = std::max( maxW, pSize->size.getWidth() ); - } else { + if ( !pSize->isLineBreak ) curX += pSize->size.getWidth(); - } } } maxW = std::max( maxW, curX ); @@ -512,18 +509,6 @@ void RichText::updateLayout() { if ( !mLines.empty() ) mLines.back().width += extraLeft; - if ( pText->isAtomic && curX > extraLeft && mMaxWidth > 0 ) { - Float extraRight = pText->margin.Right + pText->padding.Right; - Float fullTextWidth = span->getTextWidth(); - if ( curX + fullTextWidth + extraRight > mMaxWidth ) { - maxWidth = std::max( maxWidth, curX - extraLeft ); - mLines.push_back( RenderParagraph() ); - curX = extraLeft; - if ( !mLines.empty() ) - mLines.back().width += extraLeft; - } - } - Uint32 textHints = span->getTextHints(); // Compute where lines break within this text span. @@ -537,7 +522,6 @@ void RichText::updateLayout() { wrapInfo.wraps.push_back( span->getString().size() ); // Emit a RenderSpan for each segment, wrapping to new lines as needed. - Float atomicMaxX = 0; for ( size_t i = 0; i < wrapInfo.wraps.size() - 1; ++i ) { size_t startIdx = wrapInfo.wraps[i]; size_t endIdx = wrapInfo.wraps[i + 1]; @@ -559,7 +543,7 @@ void RichText::updateLayout() { RenderSpan renderSpan{ SpanBlock{ renderSpanText, pText->margin, pText->padding, - pText->lineHeight, pText->isAtomic }, + pText->lineHeight }, { curX, 0 }, Sizef( spanWidth, height ), curCharIdx, @@ -574,8 +558,6 @@ void RichText::updateLayout() { curX += spanWidth; currentLine.width += spanWidth; - if ( pText->isAtomic ) - atomicMaxX = std::max( atomicMaxX, curX ); } // After the last segment, add trailing margin and check if the @@ -584,8 +566,6 @@ void RichText::updateLayout() { Float extraRight = pText->margin.Right + pText->padding.Right; curX += extraRight; mLines.back().width += extraRight; - if ( pText->isAtomic ) - atomicMaxX = std::max( atomicMaxX, curX ); if ( !isNewline && mMaxWidth > 0 && curX > mMaxWidth ) { maxWidth = std::max( maxWidth, curX ); mLines.push_back( RenderParagraph() ); @@ -609,43 +589,28 @@ void RichText::updateLayout() { curX = 0; } } - - // Atomic (inline-block) spans reserve the width of their widest line - // so subsequent content does not flow beside a shorter last line. - if ( pText->isAtomic && atomicMaxX > curX ) { - curX = atomicMaxX; - if ( !mLines.empty() ) - mLines.back().width = std::max( mLines.back().width, curX ); - } - - // If the inline-block spanned multiple lines, force a new line - // so trailing content starts below the entire block. - if ( pText->isAtomic && wrapInfo.wraps.size() > 2 && curX > 0 ) { - maxWidth = std::max( maxWidth, curX ); - mLines.push_back( RenderParagraph() ); - curX = 0; - } } else { // Drawable or CustomBlock (non-float). Sizef blockSize; - bool isBlock = false; + bool isLineBreak = false; if ( auto pDrawable = std::get_if>( &block ) ) { auto& drawable = *pDrawable; blockSize = drawable ? drawable->getPixelsSize() : Sizef(); } else if ( auto pSize = std::get_if( &block ) ) { blockSize = pSize->size; - isBlock = pSize->isBlock; + isLineBreak = pSize->isLineBreak; } - // Block elements force a line break before themselves. - if ( isBlock && curX > 0 ) { + if ( isLineBreak ) { maxWidth = std::max( maxWidth, curX ); - mLines.push_back( RenderParagraph() ); + if ( !mLines.back().spans.empty() ) + mLines.push_back( RenderParagraph() ); curX = 0; + continue; } // Inline elements that don't fit wrap to the next line. - if ( mMaxWidth > 0 && !isBlock && + if ( mMaxWidth > 0 && ( curX + blockSize.getWidth() >= mMaxWidth || curX >= mMaxWidth ) && curX > 0 ) { maxWidth = std::max( maxWidth, curX ); @@ -665,8 +630,7 @@ void RichText::updateLayout() { curX += blockSize.getWidth(); currentLine.width += blockSize.getWidth(); - // Block elements also force a line break after themselves. - if ( ( mMaxWidth > 0 && curX >= mMaxWidth ) || isBlock ) { + if ( mMaxWidth > 0 && curX >= mMaxWidth ) { maxWidth = std::max( maxWidth, curX ); mLines.push_back( RenderParagraph() ); curX = 0; @@ -768,6 +732,19 @@ void RichText::updateLayout() { return floatRightEdge( y ) - floatLeftEdge( y ); }; + auto activeFloatBottom = [&]( Float y ) -> Float { + Float bottom = y; + for ( const auto& f : leftFloats ) { + if ( y >= f.Top && y < f.Bottom ) + bottom = std::max( bottom, f.Bottom ); + } + for ( const auto& f : rightFloats ) { + if ( y >= f.Top && y < f.Bottom ) + bottom = std::max( bottom, f.Bottom ); + } + return bottom; + }; + // Advances curY past the bottom of active floats specified by clearType. // Returns true if curY was moved. auto clearFloats = [&]( UI::CSSClear clearType ) -> bool { @@ -819,18 +796,6 @@ void RichText::updateLayout() { if ( !mLines.empty() ) mLines.back().width += extraLeft; - if ( pText->isAtomic && curX > extraLeft && mMaxWidth > 0 ) { - Float extraRight = pText->margin.Right + pText->padding.Right; - Float fullTextWidth = span->getTextWidth(); - if ( curX + fullTextWidth + extraRight > mMaxWidth ) { - maxWidth = std::max( maxWidth, curX - extraLeft ); - mLines.push_back( RenderParagraph() ); - curX = extraLeft; - if ( !mLines.empty() ) - mLines.back().width += extraLeft; - } - } - // Shift curX inside to the left edge — text starts // to the right of any left floats. Float le = floatLeftEdge( curY ); @@ -852,7 +817,6 @@ void RichText::updateLayout() { wrapInfo.wraps.back() != (Float)span->getString().size() ) wrapInfo.wraps.push_back( span->getString().size() ); - Float atomicMaxX = 0; for ( size_t i = 0; i < wrapInfo.wraps.size() - 1; ++i ) { size_t startIdx = wrapInfo.wraps[i]; size_t endIdx = wrapInfo.wraps[i + 1]; @@ -871,8 +835,8 @@ void RichText::updateLayout() { Float spanWidth = renderSpanText->getTextWidth(); RenderSpan renderSpan{ - SpanBlock{ renderSpanText, pText->margin, pText->padding, pText->lineHeight, - pText->isAtomic }, + SpanBlock{ renderSpanText, pText->margin, pText->padding, + pText->lineHeight }, { curX, 0 }, Sizef( spanWidth, height ), curCharIdx, @@ -888,8 +852,6 @@ void RichText::updateLayout() { curX += spanWidth; currentLine.width += spanWidth; - if ( pText->isAtomic ) - atomicMaxX = std::max( atomicMaxX, curX ); } // After the last segment, add trailing margin and check if the @@ -898,8 +860,6 @@ void RichText::updateLayout() { Float extraRight = pText->margin.Right + pText->padding.Right; curX += extraRight; mLines.back().width += extraRight; - if ( pText->isAtomic ) - atomicMaxX = std::max( atomicMaxX, curX ); if ( effW > 0 && effW < 1e9f && curX > effW ) { maxWidth = std::max( maxWidth, curX ); mLines.push_back( RenderParagraph() ); @@ -923,25 +883,10 @@ void RichText::updateLayout() { curX = 0; } } - - // Atomic (inline-block) spans reserve the width of their widest line. - if ( pText->isAtomic && atomicMaxX > curX ) { - curX = atomicMaxX; - if ( !mLines.empty() ) - mLines.back().width = std::max( mLines.back().width, curX ); - } - - // If the inline-block spanned multiple lines, force a new line - // so trailing content starts below the entire block. - if ( pText->isAtomic && wrapInfo.wraps.size() > 2 && curX > 0 ) { - maxWidth = std::max( maxWidth, curX ); - mLines.push_back( RenderParagraph() ); - curX = 0; - } } else { // ── Drawable or CustomBlock ──────────────────────────── Sizef blockSize; - bool isBlock = false; + bool isLineBreak = false; UI::CSSFloat floatType = UI::CSSFloat::None; UI::CSSClear clearType = UI::CSSClear::None; if ( auto pDrawable = std::get_if>( &block ) ) { @@ -949,9 +894,20 @@ void RichText::updateLayout() { blockSize = drawable ? drawable->getPixelsSize() : Sizef(); } else if ( auto pSize = std::get_if( &block ) ) { blockSize = pSize->size; - isBlock = pSize->isBlock; floatType = pSize->floatType; clearType = pSize->clearType; + isLineBreak = pSize->isLineBreak; + } + + if ( isLineBreak ) { + maxWidth = std::max( maxWidth, curX ); + if ( !mLines.back().spans.empty() ) { + curY += mLines.back().height; + mLines.push_back( RenderParagraph() ); + mLines.back().y = curY; + } + curX = 0; + continue; } // ── Clear: advance curY past active floats ───────────── @@ -959,6 +915,7 @@ void RichText::updateLayout() { if ( clearFloats( clearType ) ) { maxWidth = std::max( maxWidth, curX ); mLines.push_back( RenderParagraph() ); + mLines.back().y = curY; curX = 0; } } @@ -988,6 +945,7 @@ void RichText::updateLayout() { mLines.push_back( RenderParagraph() ); curX = 0; curY = maxBottom; + mLines.back().y = curY; posX = floatLeftEdge( curY ); } } @@ -1005,6 +963,7 @@ void RichText::updateLayout() { mLines.push_back( RenderParagraph() ); curX = 0; curY = maxBottom; + mLines.back().y = curY; re = floatRightEdge( curY ); le = floatLeftEdge( curY ); posX = re - blockSize.getWidth(); @@ -1028,45 +987,28 @@ void RichText::updateLayout() { rightFloats.push_back( fr ); } else { // ── Normal (non-float) block ──────────────────── - Float flowX = curX; if ( curX < le ) curX = le; - // Block elements force a line break before - // only when there is inline-flow content on the line. - if ( isBlock && flowX > 0 ) { - maxWidth = std::max( maxWidth, flowX ); - mLines.push_back( RenderParagraph() ); - curX = 0; - if ( curX < le ) - curX = le; - } - Float effW = effectiveMaxWidthAt( curY ); - // When a block does not fit beside active floats, - // advance curY below them. - if ( isBlock && effW > 0 && effW < 1e9f && - curX + blockSize.getWidth() > effW + 0.01f && curX > 0 ) { - Float maxBottom = curY; - for ( auto& f : leftFloats ) - maxBottom = std::max( maxBottom, f.Bottom ); - for ( auto& f : rightFloats ) - maxBottom = std::max( maxBottom, f.Bottom ); + if ( effW > 0 && effW < 1e9f && blockSize.getWidth() > effW + 0.01f ) { + Float maxBottom = activeFloatBottom( curY ); if ( maxBottom > curY ) { maxWidth = std::max( maxWidth, curX ); - mLines.push_back( RenderParagraph() ); + if ( !mLines.back().spans.empty() ) + mLines.push_back( RenderParagraph() ); curX = 0; curY = maxBottom; + mLines.back().y = curY; le = floatLeftEdge( curY ); - if ( curX < le ) - curX = le; + effW = effectiveMaxWidthAt( curY ); } } // Wrap if the block doesn't fit in the available width // (narrowed by active floats). - if ( effW > 0 && effW < 1e9f && !isBlock && + if ( effW > 0 && effW < 1e9f && ( curX + blockSize.getWidth() >= effW || curX >= effW ) && curX > 0 ) { maxWidth = std::max( maxWidth, curX ); mLines.push_back( RenderParagraph() ); @@ -1085,8 +1027,7 @@ void RichText::updateLayout() { curX += blockSize.getWidth(); currentLine.width += blockSize.getWidth(); - // Block elements or overflow force a line break after. - if ( ( effW > 0 && effW < 1e9f && curX >= effW ) || isBlock ) { + if ( effW > 0 && effW < 1e9f && curX >= effW ) { maxWidth = std::max( maxWidth, curX ); mLines.push_back( RenderParagraph() ); curX = 0; @@ -1106,6 +1047,8 @@ void RichText::updateLayout() { // text-align only affects inline-flow content, not floated elements. Float accumY = 0; for ( auto& line : mLines ) { + if ( line.y > accumY ) + accumY = line.y; line.y = accumY; Float xOffset = 0; @@ -1136,8 +1079,12 @@ void RichText::updateLayout() { if ( offsetY < 0 ) offsetY = 0; // Float spans keep their edge-aligned x; only inline-flow spans shift. - if ( !isFloat ) + if ( isFloat ) { + span.position.y = 0; + continue; + } else { span.position.x += xOffset; + } span.position.y = offsetY; maxLineHeight = std::max( maxLineHeight, offsetY + span.size.getHeight() ); } @@ -1149,7 +1096,13 @@ void RichText::updateLayout() { accumY += line.height; } - mSize = Sizef( maxWidth, accumY ); + Float floatBoundsBottom = 0; + for ( const auto& f : leftFloats ) + floatBoundsBottom = std::max( floatBoundsBottom, f.Bottom ); + for ( const auto& f : rightFloats ) + floatBoundsBottom = std::max( floatBoundsBottom, f.Bottom ); + + mSize = Sizef( maxWidth, std::max( accumY, floatBoundsBottom ) ); mTotalCharacterCount = curCharIdx; mNeedsLayoutUpdate = false; } diff --git a/src/eepp/ui/blocklayouter.cpp b/src/eepp/ui/blocklayouter.cpp index 60c33f5dc..f3c128c7b 100644 --- a/src/eepp/ui/blocklayouter.cpp +++ b/src/eepp/ui/blocklayouter.cpp @@ -159,8 +159,10 @@ void BlockLayouter::positionRichTextChildren( Graphics::RichText* rt ) { while ( currentSpan < line.spans.size() ) { const auto& span = line.spans[currentSpan]; currentSpan++; - if ( std::holds_alternative( span.block ) ) - return &span; + if ( auto custom = std::get_if( &span.block ) ) { + if ( !custom->isLineBreak ) + return &span; + } } currentSpan = 0; currentLine++; @@ -351,21 +353,6 @@ void BlockLayouter::positionRichTextChildren( Graphics::RichText* rt ) { widget->setPixelsPosition( targetPos - offset ); bounds = Rectf( targetPos, span->size ); - - if ( widget->isType( UI_TYPE_TEXTSPAN ) && - widget->asType()->isInlineBlock() ) { - Rectf pad = widget->getPixelsPadding(); - bounds.Left -= pad.Left; - bounds.Top -= pad.Top; - bounds.Right += pad.Right; - bounds.Bottom += pad.Bottom; - Vector2f boundsPos = bounds.getPosition(); - widget->setPixelsPosition( boundsPos - offset ); - if ( bounds.getSize() != widget->getPixelsSize() ) { - widget->setPixelsSize( bounds.getSize() ); - mResizedCount++; - } - } } } } diff --git a/src/eepp/ui/uihtmlinput.cpp b/src/eepp/ui/uihtmlinput.cpp index 8b3cd021a..8796b1cf0 100644 --- a/src/eepp/ui/uihtmlinput.cpp +++ b/src/eepp/ui/uihtmlinput.cpp @@ -17,6 +17,7 @@ UIHTMLInput* UIHTMLInput::New() { UIHTMLInput::UIHTMLInput() : UIHTMLWidget( "input" ) { mFlags |= UI_HTML_ELEMENT; + mDisplay = CSSDisplay::InlineBlock; mWidthPolicy = SizePolicy::WrapContent; mHeightPolicy = SizePolicy::WrapContent; createChildWidget(); diff --git a/src/eepp/ui/uihtmltextarea.cpp b/src/eepp/ui/uihtmltextarea.cpp index 43a311015..1dcff9f9f 100644 --- a/src/eepp/ui/uihtmltextarea.cpp +++ b/src/eepp/ui/uihtmltextarea.cpp @@ -25,6 +25,10 @@ bool UIHTMLTextArea::isType( const Uint32& type ) const { return UIHTMLTextArea::getType() == type || UITextEdit::isType( type ); } +bool UIHTMLTextArea::isInlineDisplay() const { + return true; +} + bool UIHTMLTextArea::applyProperty( const StyleSheetProperty& attribute ) { if ( !attribute.getPropertyDefinition() ) return false; diff --git a/src/eepp/ui/uirichtext.cpp b/src/eepp/ui/uirichtext.cpp index 8f773c61b..0b051542b 100644 --- a/src/eepp/ui/uirichtext.cpp +++ b/src/eepp/ui/uirichtext.cpp @@ -1,4 +1,5 @@ #include +#include #include #include #include @@ -291,6 +292,13 @@ void UIRichText::draw() { mSize.getHeight() - contentOffset.Top - contentOffset.Bottom ); } + if ( isType( UI_TYPE_TEXTSPAN ) && !asType()->isMergeable() && + asType()->getFontBackgroundColor() != Color::Transparent ) { + Primitives p; + p.setColor( asType()->getFontBackgroundColor() ); + p.drawRectangle( Rectf( mScreenPos.trunc(), mSize.floor() ), 0.f, Vector2f::One ); + } + mRichText.draw( std::trunc( mScreenPos.x ) + (int)contentOffset.Left, std::trunc( mScreenPos.y ) + (int)contentOffset.Top, Vector2f::One, 0.f, getBlendMode() ); @@ -824,7 +832,14 @@ void UIRichText::rebuildRichText( UILayout* container, RichText& richText, Intri : true; bool lastSpanEndsWithSpace = false; Float maxWidth = 0; - if ( container->getLayoutWidthPolicy() == SizePolicy::WrapContent ) { + bool isInlineBlockTextSpan = + container->isType( UI_TYPE_TEXTSPAN ) && container->asType()->isInlineBlock(); + if ( isInlineBlockTextSpan && mode == IntrinsicMode::None && + container->getPixelsSize().getWidth() > 0 ) { + maxWidth = container->getPixelsSize().getWidth() - + container->getPixelsContentOffset().Left - + container->getPixelsContentOffset().Right; + } else if ( container->getLayoutWidthPolicy() == SizePolicy::WrapContent ) { maxWidth = container->getMatchParentWidth() - container->getPixelsContentOffset().Left - container->getPixelsContentOffset().Right; } else { @@ -859,7 +874,9 @@ void UIRichText::rebuildRichText( UILayout* container, RichText& richText, Intri if ( !selfSpan->getText().empty() && !selfSpan->isMergeable() && NULL != selfSpan->getFontStyleConfig().Font ) { String::View selfText = selfSpan->getText().view(); - richText.addSpan( selfText, selfSpan->getFontStyleConfig() ); + FontStyleConfig style = selfSpan->getFontStyleConfig(); + style.BackgroundColor = Color::Transparent; + richText.addSpan( selfText, style ); if ( shouldCollapse ) lastSpanEndsWithSpace = !selfText.empty() && selfText.back() == ' '; } @@ -992,7 +1009,7 @@ void UIRichText::rebuildRichText( UILayout* container, RichText& richText, Intri if ( !spanText.empty() ) { richText.addSpan( spanText, span->getFontStyleConfig(), margin, padding, - spanLineHeight, span->isInlineBlock() ); + spanLineHeight ); span->setLayoutCharCount( spanText.length() ); if ( shouldCollapse ) lastSpanEndsWithSpace = spanText.back() == ' '; @@ -1065,8 +1082,7 @@ void UIRichText::rebuildRichText( UILayout* container, RichText& richText, Intri } if ( widget->isType( UI_TYPE_TEXTSPAN ) && - widget->asType()->isInlineBlock() && - widget->getPixelsSize().getWidth() == 0 ) + widget->asType()->isInlineBlock() ) widget->asType()->updateLayout(); } @@ -1094,10 +1110,28 @@ void UIRichText::rebuildRichText( UILayout* container, RichText& richText, Intri floatType = widget->asType()->getCSSFloat(); clearType = widget->asType()->getCSSClear(); } + bool isNormalFlowBlock = isBlock && floatType == CSSFloat::None; + + if ( isNormalFlowBlock ) + richText.addLineBreak(); richText.addCustomSize( Sizef( w + margin.Left + margin.Right, size.getHeight() + margin.Top + margin.Bottom ), - isBlock, floatType, clearType ); + floatType, clearType ); + + if ( widget->isType( UI_TYPE_TEXTSPAN ) && + widget->asType()->isInlineBlock() && + widget->asType()->getRichTextPtr() ) + widget->asType()->getRichTextPtr()->updateLayout(); + + if ( isNormalFlowBlock ) + richText.addLineBreak(); + else if ( widget->isType( UI_TYPE_TEXTSPAN ) && + widget->asType()->isInlineBlock() && + widget->asType()->getRichTextPtr() && + widget->asType()->getRichTextPtr()->getLines().size() > 1 ) + richText.addLineBreak(); + lastSpanEndsWithSpace = false; } } diff --git a/src/eepp/ui/uitextspan.cpp b/src/eepp/ui/uitextspan.cpp index 6162b78b7..c25f3d611 100644 --- a/src/eepp/ui/uitextspan.cpp +++ b/src/eepp/ui/uitextspan.cpp @@ -62,38 +62,13 @@ bool UITextSpan::isType( const Uint32& type ) const { } bool UITextSpan::isMergeable() const { - if ( mDisplay == CSSDisplay::Inline ) - return true; - if ( mDisplay == CSSDisplay::InlineBlock ) { - if ( getText().empty() || NULL == getFontStyleConfig().Font ) - return false; - if ( getLayoutWidthPolicy() == SizePolicy::Fixed || - getLayoutHeightPolicy() == SizePolicy::Fixed ) - return false; - return true; - } - return false; + return mDisplay == CSSDisplay::Inline; } bool UITextSpan::isInlineBlock() const { return mDisplay == CSSDisplay::InlineBlock; } -void UITextSpan::drawBorder() { - if ( ( mFlags & UI_BORDER ) && NULL != mBorder ) { - mBorder->setAlpha( mAlpha ); - if ( isInlineBlock() ) { - mBorder->draw( Vector2f( std::trunc( mScreenPos.x ), std::trunc( mScreenPos.y ) ), - Sizef( std::floor( mSize.x ), std::floor( mSize.y ) ) ); - } else { - mBorder->draw( { std::trunc( mScreenPos.x - mPaddingPx.Left ), - std::trunc( mScreenPos.y - mPaddingPx.Top ) }, - { std::floor( mSize.x + mPaddingPx.Left + mPaddingPx.Right ), - std::floor( mSize.y + mPaddingPx.Top + mPaddingPx.Bottom ) } ); - } - } -} - void UITextSpan::draw() { if ( !isMergeable() ) UIRichText::draw(); diff --git a/src/tests/unit_tests/richtext_tests.cpp b/src/tests/unit_tests/richtext_tests.cpp index 7ab26093f..96cebf282 100644 --- a/src/tests/unit_tests/richtext_tests.cpp +++ b/src/tests/unit_tests/richtext_tests.cpp @@ -396,6 +396,23 @@ UTEST( UIRichText, IntegrationAndLayoutVerification ) { destroyRichTextScene( sceneNode ); } +UTEST( RichText, VirtualLineBreakSeparatesCustomBlocks ) { + RichText rt; + rt.addCustomSize( { 10, 5 } ); + rt.addLineBreak(); + rt.addCustomSize( { 20, 7 } ); + rt.updateLayout(); + + const auto& lines = rt.getLines(); + ASSERT_EQ( lines.size(), (size_t)2 ); + ASSERT_EQ( lines[0].spans.size(), (size_t)1 ); + ASSERT_EQ( lines[1].spans.size(), (size_t)1 ); + EXPECT_EQ( lines[0].spans[0].position.x, 0 ); + EXPECT_EQ( lines[1].spans[0].position.x, 0 ); + EXPECT_EQ( lines[1].y, lines[0].height ); + EXPECT_EQ( rt.getSize().getHeight(), lines[0].height + lines[1].height ); +} + UTEST( UIRichText, selection ) { auto sceneNode = createRichTextScene(); ASSERT_TRUE( sceneNode != nullptr ); diff --git a/src/tests/unit_tests/uihtml_tests.cpp b/src/tests/unit_tests/uihtml_tests.cpp index a61bb608e..d70a29d62 100644 --- a/src/tests/unit_tests/uihtml_tests.cpp +++ b/src/tests/unit_tests/uihtml_tests.cpp @@ -541,6 +541,40 @@ UTEST( UIHTMLTextArea, rowsColsAttribute ) { Engine::destroySingleton(); } +UTEST( UIHTML, FormControlsDefaultInlineBlock ) { + init_ui_test(); + auto* sceneNode = SceneManager::instance()->getUISceneNode(); + sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( R"html( +
+ + + + +
+ )html" ) ); + sceneNode->updateDirtyLayouts(); + + auto* i1 = sceneNode->getRoot()->find( "i1" )->asType(); + auto* i2 = sceneNode->getRoot()->find( "i2" )->asType(); + auto* t1 = sceneNode->getRoot()->find( "t1" )->asType(); + auto* t2 = sceneNode->getRoot()->find( "t2" )->asType(); + + ASSERT_TRUE( i1 != nullptr ); + ASSERT_TRUE( i2 != nullptr ); + ASSERT_TRUE( t1 != nullptr ); + ASSERT_TRUE( t2 != nullptr ); + + EXPECT_EQ( i1->getDisplay(), CSSDisplay::InlineBlock ); + EXPECT_EQ( i2->getDisplay(), CSSDisplay::InlineBlock ); + + EXPECT_EQ( i1->getPixelsPosition().y, i2->getPixelsPosition().y ); + EXPECT_LT( i1->getPixelsPosition().x, i2->getPixelsPosition().x ); + EXPECT_EQ( t1->getPixelsPosition().y, t2->getPixelsPosition().y ); + EXPECT_LT( t1->getPixelsPosition().x, t2->getPixelsPosition().x ); + + Engine::destroySingleton(); +} + UTEST( UIHTMLTable, tableLayoutFixed ) { Engine::instance()->createWindow( WindowSettings( 1024, 650, "HTML Tables Test", WindowStyle::Default, WindowBackend::Default,