diff --git a/bin/unit_tests/assets/html/eepp-ui-anchor-margins.webp b/bin/unit_tests/assets/html/eepp-ui-anchor-margins.webp index ac9873391..f501815a6 100644 Binary files a/bin/unit_tests/assets/html/eepp-ui-anchor-margins.webp and b/bin/unit_tests/assets/html/eepp-ui-anchor-margins.webp differ diff --git a/bin/unit_tests/assets/html/eepp-ui-span-padding.webp b/bin/unit_tests/assets/html/eepp-ui-span-padding.webp index 63545e563..2fcadba2b 100644 Binary files a/bin/unit_tests/assets/html/eepp-ui-span-padding.webp and b/bin/unit_tests/assets/html/eepp-ui-span-padding.webp differ diff --git a/include/eepp/graphics/richtext.hpp b/include/eepp/graphics/richtext.hpp index e746b8fd5..84a08c182 100644 --- a/include/eepp/graphics/richtext.hpp +++ b/include/eepp/graphics/richtext.hpp @@ -155,6 +155,19 @@ class EE_API RichText : public Drawable { /** @return The list of rendered lines. */ const std::vector& getLines() const { return mLines; } + /** Sets line-height as a multiplier of the font's default line spacing. + * Use 0 to reset to font default. */ + void setLineHeight( Float height ); + + /** @return The line height multiplier. */ + Float getLineHeight() const { return mLineHeight; } + + /** Sets text-indent (pixels) for the first line. */ + void setTextIndent( Float indent ); + + /** @return The text indent in pixels. */ + Float getTextIndent() const { return mTextIndent; } + /** @brief Sets the text selection range. */ void setSelection( TextSelectionRange range ); @@ -207,6 +220,8 @@ class EE_API RichText : public Drawable { Sizef mSize; Int64 mTotalCharacterCount{ 0 }; bool mNeedsLayoutUpdate{ true }; + Float mLineHeight{ 0 }; + Float mTextIndent{ 0 }; }; }} // namespace EE::Graphics diff --git a/include/eepp/ui/css/propertydefinition.hpp b/include/eepp/ui/css/propertydefinition.hpp index f60eddee0..a82a3a822 100644 --- a/include/eepp/ui/css/propertydefinition.hpp +++ b/include/eepp/ui/css/propertydefinition.hpp @@ -220,6 +220,8 @@ enum class PropertyId : Uint32 { TextAsFallback = String::hash( "text-as-fallback" ), SelectOnClick = String::hash( "select-on-click" ), LineSpacing = String::hash( "line-spacing" ), + LineHeight = String::hash( "line-height" ), + TextIndent = String::hash( "text-indent" ), GravityOwner = String::hash( "gravity-owner" ), Href = String::hash( "href" ), Focusable = String::hash( "focusable" ), diff --git a/include/eepp/ui/uirichtext.hpp b/include/eepp/ui/uirichtext.hpp index 59e6b07d4..1302d66a3 100644 --- a/include/eepp/ui/uirichtext.hpp +++ b/include/eepp/ui/uirichtext.hpp @@ -113,6 +113,14 @@ class EE_API UIRichText : public UIHTMLWidget { UIRichText* setTextAlign( const Uint32& align ); + Float getLineHeightPx() const { return mLineHeightPx; } + + UIRichText* setLineHeightPx( Float height ); + + Float getTextIndentPx() const { return mTextIndentPx; } + + UIRichText* setTextIndentPx( Float indent ); + bool isTextSelectionEnabled() const; void setTextSelectionEnabled( bool active ); @@ -138,6 +146,8 @@ class EE_API UIRichText : public UIHTMLWidget { Int64 mSelCurInit{ 0 }; Int64 mSelCurEnd{ 0 }; bool mSelecting{ false }; + Float mLineHeightPx{ 0 }; + Float mTextIndentPx{ 0 }; explicit UIRichText( const std::string& tag = "richtext" ); diff --git a/src/eepp/graphics/richtext.cpp b/src/eepp/graphics/richtext.cpp index e0c458ffe..00a2873a3 100644 --- a/src/eepp/graphics/richtext.cpp +++ b/src/eepp/graphics/richtext.cpp @@ -130,6 +130,20 @@ void RichText::setSelection( TextSelectionRange range ) { } } +void RichText::setLineHeight( Float height ) { + if ( mLineHeight != height ) { + mLineHeight = height; + invalidate(); + } +} + +void RichText::setTextIndent( Float indent ) { + if ( mTextIndent != indent ) { + mTextIndent = indent; + invalidate(); + } +} + void RichText::setSelectionColor( const Color& color ) { mSelectionColor = color; } @@ -466,7 +480,7 @@ void RichText::updateLayout() { mLines.clear(); mLines.push_back( RenderParagraph() ); - Float curX = 0; + Float curX = mTextIndent; Float maxWidth = 0; Int64 curCharIdx = 0; @@ -524,7 +538,9 @@ void RichText::updateLayout() { renderSpanText->setStyleConfig( fontStyle ); Float ascent = fontStyle.Font->getAscent( fontStyle.CharacterSize ); - Float height = fontStyle.Font->getLineSpacing( fontStyle.CharacterSize ); + Float height = mLineHeight > 0 + ? mLineHeight + : fontStyle.Font->getLineSpacing( fontStyle.CharacterSize ); Float spanWidth = renderSpanText->getTextWidth(); RenderSpan renderSpan; @@ -686,7 +702,7 @@ void RichText::updateLayout() { mLines.clear(); mLines.push_back( RenderParagraph() ); - Float curX = 0; + Float curX = mTextIndent; Float maxWidth = 0; Int64 curCharIdx = 0; @@ -805,7 +821,9 @@ void RichText::updateLayout() { renderSpanText->setStyleConfig( fontStyle ); Float ascent = fontStyle.Font->getAscent( fontStyle.CharacterSize ); - Float height = fontStyle.Font->getLineSpacing( fontStyle.CharacterSize ); + Float height = mLineHeight > 0 + ? mLineHeight + : fontStyle.Font->getLineSpacing( fontStyle.CharacterSize ); Float spanWidth = renderSpanText->getTextWidth(); RenderSpan renderSpan; diff --git a/src/eepp/ui/css/stylesheetspecification.cpp b/src/eepp/ui/css/stylesheetspecification.cpp index 8d322d526..9420e8900 100644 --- a/src/eepp/ui/css/stylesheetspecification.cpp +++ b/src/eepp/ui/css/stylesheetspecification.cpp @@ -229,6 +229,8 @@ void StyleSheetSpecification::registerDefaultProperties() { registerProperty( "font-style", "", true ).addAlias( "font-weight" ); registerProperty( "text-decoration", "", true ); registerProperty( "line-spacing", "", true ).setType( PropertyType::NumberLength ); + registerProperty( "line-height", "", true ).setType( PropertyType::NumberLength ); + registerProperty( "text-indent", "", true ).setType( PropertyType::NumberLength ); registerProperty( "text-stroke-width", "", true ) .setType( PropertyType::NumberLength ) .addAlias( "fontoutlinethickness" ); diff --git a/src/eepp/ui/uirichtext.cpp b/src/eepp/ui/uirichtext.cpp index 61550af58..9dde96bb3 100644 --- a/src/eepp/ui/uirichtext.cpp +++ b/src/eepp/ui/uirichtext.cpp @@ -316,6 +316,33 @@ bool UIRichText::applyProperty( const StyleSheetProperty& attribute ) { mDataProperties["data-language"] = attribute; break; } + case PropertyId::LineHeight: { + std::string val = attribute.value(); + if ( val == "normal" || val.empty() ) { + setLineHeightPx( 0 ); + } else { + // Unitless number: multiplier of font size + bool isUnitless = !val.empty(); + for ( char c : val ) { + if ( c != '-' && c != '+' && c != '.' && !String::isNumber( c, false ) ) { + isUnitless = false; + break; + } + } + + if ( isUnitless ) { + Float multiplier = StyleSheetLength::fromString( val, 0 ).getValue(); + setLineHeightPx( multiplier * getFontSize() ); + } else { + setLineHeightPx( lengthFromValue( attribute ) ); + } + } + break; + } + case PropertyId::TextIndent: { + setTextIndentPx( lengthFromValue( attribute ) ); + break; + } default: return UIHTMLWidget::applyProperty( attribute ); } @@ -360,6 +387,10 @@ std::string UIRichText::getPropertyString( const PropertyDefinition* propertyDef return getTextAlign() == TEXT_ALIGN_CENTER ? "center" : ( getTextAlign() == TEXT_ALIGN_RIGHT ? "right" : "left" ); + case PropertyId::LineHeight: + return mLineHeightPx > 0 ? String::fromFloat( mLineHeightPx, "px" ) : "normal"; + case PropertyId::TextIndent: + return mTextIndentPx > 0 ? String::fromFloat( mTextIndentPx, "px" ) : "0"; default: return UIHTMLWidget::getPropertyString( propertyDef, propertyIndex ); } @@ -372,7 +403,7 @@ 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::TextDecoration, PropertyId::LineHeight, PropertyId::TextIndent }; props.insert( props.end(), local.begin(), local.end() ); return props; } @@ -542,6 +573,24 @@ UIRichText* UIRichText::setTextAlign( const Uint32& align ) { return this; } +UIRichText* UIRichText::setLineHeightPx( Float height ) { + if ( mLineHeightPx != height ) { + mLineHeightPx = height; + notifyLayoutAttrChange(); + notifyLayoutAttrChangeParent(); + } + return this; +} + +UIRichText* UIRichText::setTextIndentPx( Float indent ) { + if ( mTextIndentPx != indent ) { + mTextIndentPx = indent; + notifyLayoutAttrChange(); + notifyLayoutAttrChangeParent(); + } + return this; +} + void UIRichText::loadFromXmlNode( const pugi::xml_node& node ) { beginAttributesTransaction(); @@ -657,6 +706,11 @@ String UIRichText::UIRichText::collapseInternalWhitespace( const String& s ) { void UIRichText::rebuildRichText( UILayout* container, RichText& richText, IntrinsicMode mode ) { richText.clear(); + if ( container->isType( UI_TYPE_RICHTEXT ) ) { + auto* uiRt = static_cast( container ); + richText.setLineHeight( uiRt->getLineHeightPx() ); + richText.setTextIndent( uiRt->getTextIndentPx() ); + } Float maxWidth = 0; if ( container->getLayoutWidthPolicy() == SizePolicy::WrapContent ) { maxWidth = container->getMatchParentWidth() - container->getPixelsContentOffset().Left - diff --git a/src/examples/richtext/richtext.cpp b/src/examples/richtext/richtext.cpp index ce3d00266..ccc0852c4 100644 --- a/src/examples/richtext/richtext.cpp +++ b/src/examples/richtext/richtext.cpp @@ -20,6 +20,8 @@ EE_MAIN_FUNC int main( int argc, char* argv[] ) { richText.getFontStyleConfig().Font = font; richText.getFontStyleConfig().CharacterSize = 24; richText.setAlign( TEXT_ALIGN_LEFT ); + richText.setLineHeight( 36 ); // Line height of 36px (1.5x 24px font) + richText.setTextIndent( 30 ); // Indent first line 30px // Add spans using the helper method richText.addSpan( "Hello " );