diff --git a/bin/unit_tests/assets/html/eepp-uihtmltable-complex-layout-2.webp b/bin/unit_tests/assets/html/eepp-uihtmltable-complex-layout-2.webp index 1a7acfa91..f6eb34480 100644 Binary files a/bin/unit_tests/assets/html/eepp-uihtmltable-complex-layout-2.webp and b/bin/unit_tests/assets/html/eepp-uihtmltable-complex-layout-2.webp differ diff --git a/bin/unit_tests/assets/html/eepp-uihtmltable-complex-layout-3.webp b/bin/unit_tests/assets/html/eepp-uihtmltable-complex-layout-3.webp index 6a3be4b5a..d10d21890 100644 Binary files a/bin/unit_tests/assets/html/eepp-uihtmltable-complex-layout-3.webp and b/bin/unit_tests/assets/html/eepp-uihtmltable-complex-layout-3.webp differ 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 9b9f1dd90..0d96cc2b7 100644 Binary files a/bin/unit_tests/assets/html/eepp-uihtmltable-complex-layout.webp and b/bin/unit_tests/assets/html/eepp-uihtmltable-complex-layout.webp differ diff --git a/include/eepp/ui/css/propertydefinition.hpp b/include/eepp/ui/css/propertydefinition.hpp index a82a3a822..33e0a468c 100644 --- a/include/eepp/ui/css/propertydefinition.hpp +++ b/include/eepp/ui/css/propertydefinition.hpp @@ -80,6 +80,7 @@ enum class PropertyId : Uint32 { FontStyle = String::hash( "font-style" ), TextDecoration = String::hash( "text-decoration" ), Wordwrap = String::hash( "word-wrap" ), + WhiteSpaceCollapse = String::hash( "white-space-collapse" ), TextStrokeWidth = String::hash( "text-stroke-width" ), TextStrokeColor = String::hash( "text-stroke-color" ), TextSelection = String::hash( "text-selection" ), diff --git a/include/eepp/ui/uirichtext.hpp b/include/eepp/ui/uirichtext.hpp index 50cdcad6c..7d489bc46 100644 --- a/include/eepp/ui/uirichtext.hpp +++ b/include/eepp/ui/uirichtext.hpp @@ -11,6 +11,12 @@ class EE_API UIRichText : public UIHTMLWidget { public: enum class IntrinsicMode { None, Min, Max }; + enum class WhiteSpaceCollapse { Collapse, Preserve, PreserveBreaks, PreserveSpaces, BreakSpaces }; + + static WhiteSpaceCollapse toWhiteSpaceCollapse( std::string val ); + + static std::string fromWhiteSpaceCollapse( WhiteSpaceCollapse val ); + static String collapseInternalWhitespace( const String& s ); static void rebuildRichText( UILayout* container, RichText& richText, @@ -113,6 +119,10 @@ class EE_API UIRichText : public UIHTMLWidget { UIRichText* setTextAlign( const Uint32& align ); + WhiteSpaceCollapse getWhiteSpaceCollapse() const; + + void setWhiteSpaceCollapse( WhiteSpaceCollapse collapse ); + Float getLineHeightPx() const; UIRichText* setLineHeightEq( const std::string& eq ); @@ -152,6 +162,7 @@ class EE_API UIRichText : public UIHTMLWidget { mutable bool mLineHeightPxDirty{ true }; mutable Float mTextIndentPxCache{ 0 }; mutable bool mTextIndentPxDirty{ true }; + WhiteSpaceCollapse mWhiteSpaceCollapse{ WhiteSpaceCollapse::Collapse }; explicit UIRichText( const std::string& tag = "richtext" ); diff --git a/src/eepp/ui/css/stylesheetspecification.cpp b/src/eepp/ui/css/stylesheetspecification.cpp index 9420e8900..daa6acf04 100644 --- a/src/eepp/ui/css/stylesheetspecification.cpp +++ b/src/eepp/ui/css/stylesheetspecification.cpp @@ -335,6 +335,8 @@ void StyleSheetSpecification::registerDefaultProperties() { registerProperty( "word-wrap", "" ).setType( PropertyType::Bool ); + registerProperty( "white-space-collapse", "collapse", true ).setType( PropertyType::String ); + registerProperty( "hint", "" ).setType( PropertyType::String ); registerProperty( "hint-color", "" ).setType( PropertyType::Color ); registerProperty( "hint-shadow-color", "" ).setType( PropertyType::Color ); diff --git a/src/eepp/ui/uirichtext.cpp b/src/eepp/ui/uirichtext.cpp index a4b34f1c8..9001d3c32 100644 --- a/src/eepp/ui/uirichtext.cpp +++ b/src/eepp/ui/uirichtext.cpp @@ -17,6 +17,35 @@ namespace EE { namespace UI { +UIRichText::WhiteSpaceCollapse UIRichText::toWhiteSpaceCollapse( std::string val ) { + String::toLowerInPlace( val ); + if ( "preserve" == val ) + return WhiteSpaceCollapse::Preserve; + if ( "preserve-breaks" == val || "preserve-breaks" == val ) + return WhiteSpaceCollapse::PreserveBreaks; + if ( "preserve-spaces" == val || "preserve-spaces" == val ) + return WhiteSpaceCollapse::PreserveSpaces; + if ( "break-spaces" == val || "break-spaces" == val ) + return WhiteSpaceCollapse::BreakSpaces; + return WhiteSpaceCollapse::Collapse; +} + +std::string UIRichText::fromWhiteSpaceCollapse( WhiteSpaceCollapse val ) { + switch ( val ) { + case WhiteSpaceCollapse::Preserve: + return "preserve"; + case WhiteSpaceCollapse::PreserveBreaks: + return "preserve-breaks"; + case WhiteSpaceCollapse::PreserveSpaces: + return "preserve-spaces"; + case WhiteSpaceCollapse::BreakSpaces: + return "break-spaces"; + case WhiteSpaceCollapse::Collapse: + default: + return "collapse"; + } +} + UIHTMLHtml* UIHTMLHtml::New( const std::string& tag ) { return eeNew( UIHTMLHtml, ( tag ) ); } @@ -322,6 +351,9 @@ bool UIRichText::applyProperty( const StyleSheetProperty& attribute ) { case PropertyId::TextIndent: setTextIndentEq( attribute.value() ); break; + case PropertyId::WhiteSpaceCollapse: + setWhiteSpaceCollapse( toWhiteSpaceCollapse( attribute.value() ) ); + break; default: return UIHTMLWidget::applyProperty( attribute ); } @@ -370,6 +402,8 @@ std::string UIRichText::getPropertyString( const PropertyDefinition* propertyDef return mLineHeightEq.empty() ? "normal" : mLineHeightEq; case PropertyId::TextIndent: return mTextIndentEq.empty() ? "0" : mTextIndentEq; + case PropertyId::WhiteSpaceCollapse: + return fromWhiteSpaceCollapse( mWhiteSpaceCollapse ); default: return UIHTMLWidget::getPropertyString( propertyDef, propertyIndex ); } @@ -382,7 +416,8 @@ std::vector UIRichText::getPropertiesImplemented() const { PropertyId::Color, PropertyId::TextShadowColor, PropertyId::TextShadowOffset, PropertyId::TextStrokeWidth, PropertyId::TextStrokeColor, PropertyId::TextAlign, PropertyId::SelectionColor, PropertyId::SelectionBackColor, PropertyId::TextSelection, - PropertyId::TextDecoration, PropertyId::LineHeight, PropertyId::TextIndent }; + PropertyId::TextDecoration, PropertyId::LineHeight, PropertyId::TextIndent, + PropertyId::WhiteSpaceCollapse }; props.insert( props.end(), local.begin(), local.end() ); return props; } @@ -553,6 +588,18 @@ UIRichText* UIRichText::setTextAlign( const Uint32& align ) { return this; } +UIRichText::WhiteSpaceCollapse UIRichText::getWhiteSpaceCollapse() const { + return mWhiteSpaceCollapse; +} + +void UIRichText::setWhiteSpaceCollapse( WhiteSpaceCollapse collapse ) { + if ( mWhiteSpaceCollapse != collapse ) { + mWhiteSpaceCollapse = collapse; + notifyLayoutAttrChange(); + notifyLayoutAttrChangeParent(); + } +} + UIRichText* UIRichText::setLineHeightEq( const std::string& eq ) { if ( mLineHeightEq != eq ) { mLineHeightEq = eq; @@ -734,6 +781,12 @@ void UIRichText::rebuildRichText( UILayout* container, RichText& richText, Intri richText.setLineHeight( uiRt->getLineHeightPx() ); richText.setTextIndent( uiRt->getTextIndentPx() ); } + bool shouldCollapse = + container->isType( UI_TYPE_RICHTEXT ) + ? static_cast( container )->getWhiteSpaceCollapse() == + WhiteSpaceCollapse::Collapse + : true; + bool lastSpanEndsWithSpace = false; Float maxWidth = 0; if ( container->getLayoutWidthPolicy() == SizePolicy::WrapContent ) { maxWidth = container->getMatchParentWidth() - container->getPixelsContentOffset().Left - @@ -769,7 +822,10 @@ void UIRichText::rebuildRichText( UILayout* container, RichText& richText, Intri UITextSpan* selfSpan = container->asType(); if ( !selfSpan->getText().empty() && !selfSpan->isMergeable() && NULL != selfSpan->getFontStyleConfig().Font ) { - richText.addSpan( selfSpan->getText(), selfSpan->getFontStyleConfig() ); + String::View selfText = selfSpan->getText().view(); + richText.addSpan( selfText, selfSpan->getFontStyleConfig() ); + if ( shouldCollapse ) + lastSpanEndsWithSpace = !selfText.empty() && selfText.back() == ' '; } } @@ -830,6 +886,9 @@ void UIRichText::rebuildRichText( UILayout* container, RichText& richText, Intri if ( !nextIsInline && !text.empty() && text.back() == ' ' ) text = text.substr( 0, text.size() - 1 ); + if ( shouldCollapse && lastSpanEndsWithSpace && !text.empty() && text[0] == ' ' ) + text = text.substr( 1 ); + if ( text.empty() ) { textNode->setLayoutCharCount( 0 ); return; @@ -837,6 +896,9 @@ void UIRichText::rebuildRichText( UILayout* container, RichText& richText, Intri textNode->setLayoutCharCount( text.length() ); + if ( shouldCollapse ) + lastSpanEndsWithSpace = text.back() == ' '; + FontStyleConfig style; if ( node->getParent()->isType( UI_TYPE_TEXTSPAN ) ) { style = node->getParent()->asType()->getFontStyleConfig(); @@ -877,8 +939,15 @@ void UIRichText::rebuildRichText( UILayout* container, RichText& richText, Intri if ( !nextIsInline && !spanText.empty() && spanText.back() == ' ' ) spanText = spanText.substr( 0, spanText.size() - 1 ); - if ( !spanText.empty() ) + if ( shouldCollapse && lastSpanEndsWithSpace && !spanText.empty() && + spanText[0] == ' ' ) + spanText = spanText.substr( 1 ); + + if ( !spanText.empty() ) { richText.addSpan( spanText, span->getFontStyleConfig(), margin, padding ); + if ( shouldCollapse ) + lastSpanEndsWithSpace = spanText.back() == ' '; + } } else if ( margin.Left > 0 || margin.Top > 0 || padding.Left > 0 || padding.Top > 0 ) { Rectf leftOnly( margin.Left, margin.Top, 0, 0 ); Rectf padLeftOnly( padding.Left, padding.Top, 0, 0 ); @@ -903,6 +972,7 @@ void UIRichText::rebuildRichText( UILayout* container, RichText& richText, Intri } else if ( widget->isType( UI_TYPE_BR ) ) { richText.addSpan( "\n", widget->asType()->getRichText().getFontStyleConfig() ); + lastSpanEndsWithSpace = false; } else { Rectf margin = widget->getLayoutPixelsMargin(); bool isBlock = widget->getLayoutWidthPolicy() == SizePolicy::MatchParent; @@ -960,6 +1030,7 @@ void UIRichText::rebuildRichText( UILayout* container, RichText& richText, Intri richText.addCustomSize( Sizef( w + margin.Left + margin.Right, size.getHeight() + margin.Top + margin.Bottom ), isBlock, floatType, clearType ); + lastSpanEndsWithSpace = false; } };