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;
}
};