Implement CSS box-sizing for HTML layout sizing

Add CSS box-sizing support for HTML widgets and route CSS-specified
width/height conversion through virtual sizing hooks. Resolve percentage
CSS sizes against the containing block content box, and apply content-box
or border-box conversion consistently across block, flex, grid, table,
and rich text layout paths.

Keep table wrapper width behavior compatible with CSS table layout by
overriding the table width conversion separately, so padded percentage
tables do not overflow their assigned wrapper width.

Add focused regressions for block padding, flex-basis sizing, grid
containers, table containers, and the border rendering cases that depend
on explicit border-box sizing.
This commit is contained in:
Martín Lucas Golini
2026-07-06 00:32:17 -03:00
parent 2c59d9b4e2
commit e984c87105
17 changed files with 443 additions and 29 deletions

View File

@@ -377,7 +377,7 @@
"working_dir": "${project_root}/bin" "working_dir": "${project_root}/bin"
}, },
{ {
"args": "-c system --hn-dark", "args": "--hn-dark",
"command": "${project_root}/bin/eepp-ui-html-debug", "command": "${project_root}/bin/eepp-ui-html-debug",
"name": "eepp-ui-html-debug", "name": "eepp-ui-html-debug",
"working_dir": "${project_root}/bin" "working_dir": "${project_root}/bin"

View File

@@ -212,6 +212,7 @@ enum class PropertyId : Uint32 {
BorderBottomLeftRadius = String::hash( "border-bottom-left-radius" ), BorderBottomLeftRadius = String::hash( "border-bottom-left-radius" ),
BorderBottomRightRadius = String::hash( "border-bottom-right-radius" ), BorderBottomRightRadius = String::hash( "border-bottom-right-radius" ),
BorderSmooth = String::hash( "border-smooth" ), BorderSmooth = String::hash( "border-smooth" ),
BoxSizing = String::hash( "box-sizing" ),
BackgroundSmooth = String::hash( "background-smooth" ), BackgroundSmooth = String::hash( "background-smooth" ),
ForegroundSmooth = String::hash( "foreground-smooth" ), ForegroundSmooth = String::hash( "foreground-smooth" ),
TabBarHideOnSingleTab = String::hash( "tabbar-hide-on-single-tab" ), TabBarHideOnSingleTab = String::hash( "tabbar-hide-on-single-tab" ),

View File

@@ -83,6 +83,14 @@ struct EE_API CSSClearHelper {
static CSSClear fromString( std::string_view val ); static CSSClear fromString( std::string_view val );
}; };
enum class CSSBoxSizing { ContentBox, BorderBox };
struct EE_API CSSBoxSizingHelper {
static std::string toString( CSSBoxSizing val );
static CSSBoxSizing fromString( std::string_view val );
};
enum class CSSFlexDirection { Row, RowReverse, Column, ColumnReverse }; enum class CSSFlexDirection { Row, RowReverse, Column, ColumnReverse };
struct EE_API CSSFlexDirectionHelper { struct EE_API CSSFlexDirectionHelper {

View File

@@ -29,6 +29,8 @@ class EE_API UIHTMLTable : public UIHTMLWidget {
virtual bool applyProperty( const StyleSheetProperty& attribute ); virtual bool applyProperty( const StyleSheetProperty& attribute );
Float cssWidthPropertyToBorderBoxWidth( const StyleSheetProperty& property ) const;
protected: protected:
virtual Uint32 onMessage( const NodeMessage* Msg ); virtual Uint32 onMessage( const NodeMessage* Msg );

View File

@@ -83,6 +83,10 @@ class EE_API UIHTMLWidget : public UILayout {
void setCSSClear( CSSClear cssClear ); void setCSSClear( CSSClear cssClear );
CSSBoxSizing getBoxSizing() const { return mBoxSizing; }
void setBoxSizing( CSSBoxSizing boxSizing );
Rectf getNormalFlowLayoutPixelsMargin() const; Rectf getNormalFlowLayoutPixelsMargin() const;
const CSSBaselineAlignValue& getBaselineAlign() const { return mBaselineAlign; } const CSSBaselineAlignValue& getBaselineAlign() const { return mBaselineAlign; }
@@ -276,6 +280,23 @@ class EE_API UIHTMLWidget : public UILayout {
Float getBaseline() const; Float getBaseline() const;
Float getContainingBlockContentWidth() const;
Float getContainingBlockContentHeight() const;
Float lengthFromValueForCSS( const StyleSheetProperty& property,
const Float& defaultValue = 0 ) const;
Float cssResolvedLengthToBorderBoxWidth( const Float& resolvedLength ) const;
Float cssResolvedLengthToBorderBoxHeight( const Float& resolvedLength ) const;
Float cssWidthPropertyToBorderBoxWidth( const StyleSheetProperty& property ) const;
Float cssHeightPropertyToBorderBoxHeight( const StyleSheetProperty& property ) const;
void updateCSSContentBoxFixedSize();
virtual void onParentChange(); virtual void onParentChange();
virtual void onPositionChange(); virtual void onPositionChange();
@@ -322,6 +343,7 @@ class EE_API UIHTMLWidget : public UILayout {
CSSPosition mPosition{ CSSPosition::Static }; CSSPosition mPosition{ CSSPosition::Static };
CSSFloat mFloat{ CSSFloat::None }; CSSFloat mFloat{ CSSFloat::None };
CSSClear mClear{ CSSClear::None }; CSSClear mClear{ CSSClear::None };
CSSBoxSizing mBoxSizing{ CSSBoxSizing::ContentBox };
CSSBaselineAlignValue mBaselineAlign; CSSBaselineAlignValue mBaselineAlign;
CSSVisibility mVisibility{ CSSVisibility::Visible }; CSSVisibility mVisibility{ CSSVisibility::Visible };
std::string mTopEq{ "auto" }; std::string mTopEq{ "auto" };

View File

@@ -1388,6 +1388,44 @@ class EE_API UIWidget : public UINode {
/**@return The property `height` converted as length */ /**@return The property `height` converted as length */
Float getPropertyHeight() const; Float getPropertyHeight() const;
/**
* @brief Converts an already resolved CSS width value into this widget's stored border-box
* width.
*
* The base widget has no CSS box model adjustment, so the value is returned as-is. HTML
* widgets override this to account for box-sizing and content offsets.
*/
virtual Float cssResolvedLengthToBorderBoxWidth( const Float& resolvedLength ) const;
/**
* @brief Converts an already resolved CSS height value into this widget's stored border-box
* height.
*
* The base widget has no CSS box model adjustment, so the value is returned as-is. HTML
* widgets override this to account for box-sizing and content offsets.
*/
virtual Float cssResolvedLengthToBorderBoxHeight( const Float& resolvedLength ) const;
/**
* @brief Resolves a CSS width property and converts it into this widget's stored border-box
* width.
*
* This is the sizing hook layout code should use when applying a CSS-specified width. Derived
* widgets can override the relative-size resolution or box conversion without layout-specific
* type checks.
*/
virtual Float cssWidthPropertyToBorderBoxWidth( const StyleSheetProperty& property ) const;
/**
* @brief Resolves a CSS height property and converts it into this widget's stored border-box
* height.
*
* This is the sizing hook layout code should use when applying a CSS-specified height. Derived
* widgets can override the relative-size resolution or box conversion without layout-specific
* type checks.
*/
virtual Float cssHeightPropertyToBorderBoxHeight( const StyleSheetProperty& property ) const;
/* @return The width of the widget when size policy is match_parent */ /* @return The width of the widget when size policy is match_parent */
Float getMatchParentWidth() const; Float getMatchParentWidth() const;

View File

@@ -102,6 +102,22 @@ CSSPosition CSSPositionHelper::fromString( std::string_view val ) {
return position; return position;
} }
std::string CSSBoxSizingHelper::toString( CSSBoxSizing val ) {
switch ( val ) {
case CSSBoxSizing::BorderBox:
return "border-box";
case CSSBoxSizing::ContentBox:
default:
return "content-box";
}
}
CSSBoxSizing CSSBoxSizingHelper::fromString( std::string_view val ) {
if ( val == "border-box" )
return CSSBoxSizing::BorderBox;
return CSSBoxSizing::ContentBox;
}
std::string CSSListStyleTypeHelper::toString( CSSListStyleType type ) { std::string CSSListStyleTypeHelper::toString( CSSListStyleType type ) {
switch ( type ) { switch ( type ) {
case CSSListStyleType::Disc: case CSSListStyleType::Disc:

View File

@@ -108,15 +108,16 @@ void BlockLayouter::updateLayout() {
const StyleSheetProperty* prop = nullptr; const StyleSheetProperty* prop = nullptr;
if ( mContainer->getLayoutWidthPolicy() == SizePolicy::Fixed && mContainer->getUIStyle() && if ( mContainer->getLayoutWidthPolicy() == SizePolicy::Fixed && mContainer->getUIStyle() &&
( prop = mContainer->getUIStyle()->getProperty( PropertyId::Width ) ) ) { ( prop = mContainer->getUIStyle()->getProperty( PropertyId::Width ) ) ) {
mContainer->setInternalPixelsSize( mContainer->setInternalPixelsSize( { mContainer->cssWidthPropertyToBorderBoxWidth( *prop ),
{ mContainer->lengthFromValue( *prop ), mContainer->getPixelsSize().getHeight() } ); mContainer->getPixelsSize().getHeight() } );
} }
if ( !isTableCellInTableRow( mContainer ) && if ( !isTableCellInTableRow( mContainer ) &&
mContainer->getLayoutHeightPolicy() == SizePolicy::Fixed && mContainer->getUIStyle() && mContainer->getLayoutHeightPolicy() == SizePolicy::Fixed && mContainer->getUIStyle() &&
( prop = mContainer->getUIStyle()->getProperty( PropertyId::Height ) ) ) { ( prop = mContainer->getUIStyle()->getProperty( PropertyId::Height ) ) ) {
mContainer->setInternalPixelsSize( mContainer->setInternalPixelsSize(
{ mContainer->getPixelsSize().getWidth(), mContainer->lengthFromValue( *prop ) } ); { mContainer->getPixelsSize().getWidth(),
mContainer->cssHeightPropertyToBorderBoxHeight( *prop ) } );
} }
UIRichText::rebuildRichText( widget, *rt ); UIRichText::rebuildRichText( widget, *rt );
@@ -139,8 +140,8 @@ void BlockLayouter::updateLayout() {
mContainer->getParent()->isWidget() && mContainer->getParent()->isWidget() &&
mContainer->getParent()->asType<UIWidget>()->getLayoutWidthPolicy() == mContainer->getParent()->asType<UIWidget>()->getLayoutWidthPolicy() ==
SizePolicy::WrapContent ) { SizePolicy::WrapContent ) {
totW = rt->getSize().getWidth() + mContainer->getPixelsContentOffset().Left + const Rectf contentOffset = mContainer->getPixelsContentOffset();
mContainer->getPixelsContentOffset().Right; totW = rt->getSize().getWidth() + contentOffset.Left + contentOffset.Right;
if ( !mContainer->getMaxWidthEq().empty() && if ( !mContainer->getMaxWidthEq().empty() &&
totW > mContainer->getMaxSizePx().getWidth() ) totW > mContainer->getMaxSizePx().getWidth() )
mContainer->setClipType( ClipType::ContentBox ); mContainer->setClipType( ClipType::ContentBox );
@@ -167,8 +168,8 @@ void BlockLayouter::updateLayout() {
mContainer->getParent()->isWidget() && mContainer->getParent()->isWidget() &&
mContainer->getParent()->asType<UIWidget>()->getLayoutHeightPolicy() == mContainer->getParent()->asType<UIWidget>()->getLayoutHeightPolicy() ==
SizePolicy::WrapContent ) { SizePolicy::WrapContent ) {
totH = rt->getSize().getHeight() + mContainer->getPixelsContentOffset().Top + const Rectf contentOffset = mContainer->getPixelsContentOffset();
mContainer->getPixelsContentOffset().Bottom; totH = rt->getSize().getHeight() + contentOffset.Top + contentOffset.Bottom;
if ( !mContainer->getMaxHeightEq().empty() && if ( !mContainer->getMaxHeightEq().empty() &&
totH > mContainer->getMaxSizePx().getHeight() ) totH > mContainer->getMaxSizePx().getHeight() )
mContainer->setClipType( ClipType::ContentBox ); mContainer->setClipType( ClipType::ContentBox );

View File

@@ -456,6 +456,7 @@ void StyleSheetSpecification::registerDefaultProperties() {
registerProperty( "list-style-type", "none", true ).setType( PropertyType::String ); registerProperty( "list-style-type", "none", true ).setType( PropertyType::String );
registerProperty( "list-style-position", "outside", true ).setType( PropertyType::String ); registerProperty( "list-style-position", "outside", true ).setType( PropertyType::String );
registerProperty( "list-style-image", "none" ).setType( PropertyType::String ); registerProperty( "list-style-image", "none" ).setType( PropertyType::String );
registerProperty( "box-sizing", "content-box" ).setType( PropertyType::String );
registerProperty( "top", "auto" ) registerProperty( "top", "auto" )
.setType( PropertyType::NumberLength ) .setType( PropertyType::NumberLength )
.setRelativeTarget( PropertyRelativeTarget::ContainingBlockHeight ); .setRelativeTarget( PropertyRelativeTarget::ContainingBlockHeight );

View File

@@ -189,8 +189,11 @@ void FlexLayouter::readItemStyle( UIWidget* child, FlexItem& item ) {
// measureFlexItems against the flex container's inner main size. // measureFlexItems against the flex container's inner main size.
item.flexBasisValue = 0.f; item.flexBasisValue = 0.f;
} else { } else {
item.flexBasisValue = mContainer->lengthFromValue( Axis mainAxis = getMainAxis( mDirection );
val, CSS::PropertyRelativeTarget::ContainingBlockWidth, 0.f ); Float resolved = child->lengthFromValue( val, CSS::PropertyRelativeTarget::None, 0.f );
item.flexBasisValue = mainAxis.horizontal
? child->cssResolvedLengthToBorderBoxWidth( resolved )
: child->cssResolvedLengthToBorderBoxHeight( resolved );
} }
} }
@@ -220,13 +223,13 @@ Float FlexLayouter::resolveFlexBasis( UIWidget* child, CSSFlexDirection, Float f
child->getUIStyle() ) { child->getUIStyle() ) {
const auto* wprop = child->getUIStyle()->getProperty( PropertyId::Width ); const auto* wprop = child->getUIStyle()->getProperty( PropertyId::Width );
if ( wprop ) if ( wprop )
return child->lengthFromValue( *wprop ); return child->cssWidthPropertyToBorderBoxWidth( *wprop );
} }
if ( !mainAxis.horizontal && child->getLayoutHeightPolicy() == SizePolicy::Fixed && if ( !mainAxis.horizontal && child->getLayoutHeightPolicy() == SizePolicy::Fixed &&
child->getUIStyle() ) { child->getUIStyle() ) {
const auto* hprop = child->getUIStyle()->getProperty( PropertyId::Height ); const auto* hprop = child->getUIStyle()->getProperty( PropertyId::Height );
if ( hprop ) if ( hprop )
return child->lengthFromValue( *hprop ); return child->cssHeightPropertyToBorderBoxHeight( *hprop );
} }
} }
@@ -383,7 +386,11 @@ void FlexLayouter::measureFlexItems( const Axis& mainAxis, const Axis& crossAxis
String::replaceAll( pctStr, "%", "" ); String::replaceAll( pctStr, "%", "" );
Float pct = 0.f; Float pct = 0.f;
String::fromString( pct, pctStr ); String::fromString( pct, pctStr );
item.targetMainSize = containerInnerMain * pct / 100.f; Float resolved = containerInnerMain * pct / 100.f;
item.targetMainSize =
mainAxis.horizontal
? item.widget->cssResolvedLengthToBorderBoxWidth( resolved )
: item.widget->cssResolvedLengthToBorderBoxHeight( resolved );
} }
} else { } else {
item.targetMainSize = item.targetMainSize =
@@ -397,13 +404,13 @@ void FlexLayouter::measureFlexItems( const Axis& mainAxis, const Axis& crossAxis
item.widget->getUIStyle() ) { item.widget->getUIStyle() ) {
const auto* wprop = item.widget->getUIStyle()->getProperty( PropertyId::Width ); const auto* wprop = item.widget->getUIStyle()->getProperty( PropertyId::Width );
if ( wprop ) if ( wprop )
item.targetMainSize = item.widget->lengthFromValue( *wprop ); item.targetMainSize = item.widget->cssWidthPropertyToBorderBoxWidth( *wprop );
} else if ( !mainAxis.horizontal && } else if ( !mainAxis.horizontal &&
item.widget->getLayoutHeightPolicy() == SizePolicy::Fixed && item.widget->getLayoutHeightPolicy() == SizePolicy::Fixed &&
item.widget->getUIStyle() ) { item.widget->getUIStyle() ) {
const auto* hprop = item.widget->getUIStyle()->getProperty( PropertyId::Height ); const auto* hprop = item.widget->getUIStyle()->getProperty( PropertyId::Height );
if ( hprop ) if ( hprop )
item.targetMainSize = item.widget->lengthFromValue( *hprop ); item.targetMainSize = item.widget->cssHeightPropertyToBorderBoxHeight( *hprop );
} }
} }
@@ -464,13 +471,13 @@ void FlexLayouter::measureFlexItems( const Axis& mainAxis, const Axis& crossAxis
if ( item.widget->getUIStyle() ) { if ( item.widget->getUIStyle() ) {
const auto* minW = item.widget->getUIStyle()->getProperty( PropertyId::MinWidth ); const auto* minW = item.widget->getUIStyle()->getProperty( PropertyId::MinWidth );
if ( minW ) { if ( minW ) {
Float explicitMin = item.widget->lengthFromValue( *minW ); Float explicitMin = item.widget->cssWidthPropertyToBorderBoxWidth( *minW );
if ( explicitMin > item.minMainSize ) if ( explicitMin > item.minMainSize )
item.minMainSize = explicitMin; item.minMainSize = explicitMin;
} }
const auto* maxW = item.widget->getUIStyle()->getProperty( PropertyId::MaxWidth ); const auto* maxW = item.widget->getUIStyle()->getProperty( PropertyId::MaxWidth );
if ( maxW ) if ( maxW )
item.maxMainSize = item.widget->lengthFromValue( *maxW ); item.maxMainSize = item.widget->cssWidthPropertyToBorderBoxWidth( *maxW );
else else
item.maxMainSize = std::numeric_limits<Float>::max(); item.maxMainSize = std::numeric_limits<Float>::max();
} }
@@ -505,13 +512,13 @@ void FlexLayouter::measureFlexItems( const Axis& mainAxis, const Axis& crossAxis
if ( item.widget->getUIStyle() ) { if ( item.widget->getUIStyle() ) {
const auto* minH = item.widget->getUIStyle()->getProperty( PropertyId::MinHeight ); const auto* minH = item.widget->getUIStyle()->getProperty( PropertyId::MinHeight );
if ( minH ) { if ( minH ) {
Float explicitMin = item.widget->lengthFromValue( *minH ); Float explicitMin = item.widget->cssHeightPropertyToBorderBoxHeight( *minH );
if ( explicitMin > item.minMainSize ) if ( explicitMin > item.minMainSize )
item.minMainSize = explicitMin; item.minMainSize = explicitMin;
} }
const auto* maxH = item.widget->getUIStyle()->getProperty( PropertyId::MaxHeight ); const auto* maxH = item.widget->getUIStyle()->getProperty( PropertyId::MaxHeight );
if ( maxH ) if ( maxH )
item.maxMainSize = item.widget->lengthFromValue( *maxH ); item.maxMainSize = item.widget->cssHeightPropertyToBorderBoxHeight( *maxH );
else else
item.maxMainSize = std::numeric_limits<Float>::max(); item.maxMainSize = std::numeric_limits<Float>::max();
} }
@@ -1241,7 +1248,7 @@ void FlexLayouter::updateLayout() {
if ( widthPolicy == SizePolicy::Fixed && mContainer->getUIStyle() ) { if ( widthPolicy == SizePolicy::Fixed && mContainer->getUIStyle() ) {
const auto* wprop = mContainer->getUIStyle()->getProperty( PropertyId::Width ); const auto* wprop = mContainer->getUIStyle()->getProperty( PropertyId::Width );
if ( wprop ) { if ( wprop ) {
Float rawWidth = mContainer->lengthFromValue( *wprop ); Float rawWidth = widget->cssWidthPropertyToBorderBoxWidth( *wprop );
containerWidth = containerWidth =
mContainer->fitMinMaxSizePx( Sizef( rawWidth, containerHeight ) ).getWidth(); mContainer->fitMinMaxSizePx( Sizef( rawWidth, containerHeight ) ).getWidth();
} }
@@ -1250,7 +1257,7 @@ void FlexLayouter::updateLayout() {
if ( heightPolicy == SizePolicy::Fixed && mContainer->getUIStyle() ) { if ( heightPolicy == SizePolicy::Fixed && mContainer->getUIStyle() ) {
const auto* hprop = mContainer->getUIStyle()->getProperty( PropertyId::Height ); const auto* hprop = mContainer->getUIStyle()->getProperty( PropertyId::Height );
if ( hprop ) { if ( hprop ) {
Float rawHeight = mContainer->lengthFromValue( *hprop ); Float rawHeight = widget->cssHeightPropertyToBorderBoxHeight( *hprop );
containerHeight = containerHeight =
mContainer->fitMinMaxSizePx( Sizef( containerWidth, rawHeight ) ).getHeight(); mContainer->fitMinMaxSizePx( Sizef( containerWidth, rawHeight ) ).getHeight();
} }

View File

@@ -1060,7 +1060,8 @@ void GridLayouter::updateLayout() {
} }
Float resolved = best * pct / 100.f; Float resolved = best * pct / 100.f;
if ( resolved > 0.f ) { if ( resolved > 0.f ) {
mContainer->setInternalPixelsWidth( resolved ); mContainer->setInternalPixelsWidth(
grid->cssResolvedLengthToBorderBoxWidth( resolved ) );
needResize = true; needResize = true;
} }
} }

View File

@@ -16,7 +16,7 @@ static Float specifiedHeightPx( UIWidget* widget ) {
if ( prop == nullptr ) if ( prop == nullptr )
return 0.f; return 0.f;
return sanitizeFloat( widget->lengthFromValue( *prop ) ); return sanitizeFloat( widget->cssHeightPropertyToBorderBoxHeight( *prop ) );
} }
static Float normalFlowChildrenBottomPx( UIWidget* widget ) { static Float normalFlowChildrenBottomPx( UIWidget* widget ) {
@@ -327,13 +327,15 @@ void TableLayouter::updateLayout() {
if ( widget->getLayoutWidthPolicy() == SizePolicy::Fixed && widget->getUIStyle() && if ( widget->getLayoutWidthPolicy() == SizePolicy::Fixed && widget->getUIStyle() &&
( prop = widget->getUIStyle()->getProperty( PropertyId::Width ) ) ) { ( prop = widget->getUIStyle()->getProperty( PropertyId::Width ) ) ) {
widget->asType<UINode>()->setInternalPixelsSize( widget->asType<UINode>()->setInternalPixelsSize(
{ widget->lengthFromValue( *prop ), widget->getPixelsSize().getHeight() } ); { widget->cssWidthPropertyToBorderBoxWidth( *prop ),
widget->getPixelsSize().getHeight() } );
} }
if ( widget->getLayoutHeightPolicy() == SizePolicy::Fixed && widget->getUIStyle() && if ( widget->getLayoutHeightPolicy() == SizePolicy::Fixed && widget->getUIStyle() &&
( prop = widget->getUIStyle()->getProperty( PropertyId::Height ) ) ) { ( prop = widget->getUIStyle()->getProperty( PropertyId::Height ) ) ) {
widget->asType<UINode>()->setInternalPixelsSize( widget->asType<UINode>()->setInternalPixelsSize(
{ widget->getPixelsSize().getWidth(), widget->lengthFromValue( *prop ) } ); { widget->getPixelsSize().getWidth(),
widget->cssHeightPropertyToBorderBoxHeight( *prop ) } );
} }
computeIntrinsicWidths(); computeIntrinsicWidths();

View File

@@ -99,6 +99,10 @@ bool UIHTMLTable::applyProperty( const StyleSheetProperty& attribute ) {
return UIHTMLWidget::applyProperty( attribute ); return UIHTMLWidget::applyProperty( attribute );
} }
Float UIHTMLTable::cssWidthPropertyToBorderBoxWidth( const StyleSheetProperty& property ) const {
return lengthFromValueForCSS( property );
}
void UIHTMLTable::computeIntrinsicWidths() const { void UIHTMLTable::computeIntrinsicWidths() const {
UILayouter* layouter = const_cast<UIHTMLTable*>( this )->getLayouter(); UILayouter* layouter = const_cast<UIHTMLTable*>( this )->getLayouter();
if ( layouter ) if ( layouter )

View File

@@ -181,6 +181,115 @@ Float UIHTMLWidget::getBaseline() const {
return 0.f; return 0.f;
} }
Float UIHTMLWidget::getContainingBlockContentWidth() const {
Node* parent = getParent();
while ( parent && parent->isWidget() && parent->isType( UI_TYPE_HTML_WIDGET ) &&
static_cast<UIHTMLWidget*>( parent )->isInline() )
parent = parent->getParent();
if ( !parent )
return 0.f;
Float width = parent->getPixelsSize().getWidth();
if ( parent->isWidget() ) {
Rectf contentOffset = parent->asType<UIWidget>()->getPixelsContentOffset();
width -= contentOffset.Left + contentOffset.Right;
}
return eemax( 0.f, width );
}
Float UIHTMLWidget::getContainingBlockContentHeight() const {
Node* parent = getParent();
while ( parent && parent->isWidget() && parent->isType( UI_TYPE_HTML_WIDGET ) &&
static_cast<UIHTMLWidget*>( parent )->isInline() )
parent = parent->getParent();
if ( !parent )
return 0.f;
Float height = parent->getPixelsSize().getHeight();
if ( parent->isWidget() ) {
Rectf contentOffset = parent->asType<UIWidget>()->getPixelsContentOffset();
height -= contentOffset.Top + contentOffset.Bottom;
}
return eemax( 0.f, height );
}
Float UIHTMLWidget::lengthFromValueForCSS( const StyleSheetProperty& property,
const Float& defaultValue ) const {
if ( property.getPropertyDefinition() ) {
switch ( property.getPropertyDefinition()->getRelativeTarget() ) {
case PropertyRelativeTarget::ContainingBlockWidth:
return convertLength(
StyleSheetLength::fromString( property.getValue(), defaultValue ),
getContainingBlockContentWidth() );
case PropertyRelativeTarget::ContainingBlockHeight:
return convertLength(
StyleSheetLength::fromString( property.getValue(), defaultValue ),
getContainingBlockContentHeight() );
default:
break;
}
}
return lengthFromValue( property, defaultValue );
}
Float UIHTMLWidget::cssResolvedLengthToBorderBoxWidth( const Float& resolvedLength ) const {
if ( mBoxSizing == CSSBoxSizing::BorderBox )
return resolvedLength;
Rectf contentOffset = getPixelsContentOffset();
return resolvedLength + contentOffset.Left + contentOffset.Right;
}
Float UIHTMLWidget::cssResolvedLengthToBorderBoxHeight( const Float& resolvedLength ) const {
if ( mBoxSizing == CSSBoxSizing::BorderBox )
return resolvedLength;
Rectf contentOffset = getPixelsContentOffset();
return resolvedLength + contentOffset.Top + contentOffset.Bottom;
}
Float UIHTMLWidget::cssWidthPropertyToBorderBoxWidth( const StyleSheetProperty& property ) const {
return cssResolvedLengthToBorderBoxWidth( lengthFromValueForCSS( property ) );
}
Float UIHTMLWidget::cssHeightPropertyToBorderBoxHeight( const StyleSheetProperty& property ) const {
return cssResolvedLengthToBorderBoxHeight( lengthFromValueForCSS( property ) );
}
void UIHTMLWidget::updateCSSContentBoxFixedSize() {
if ( getUIStyle() == nullptr )
return;
Sizef size( getPixelsSize() );
bool changed = false;
if ( getLayoutWidthPolicy() == SizePolicy::Fixed ) {
const auto* width = getUIStyle()->getProperty( PropertyId::Width );
if ( width && width->value() != "auto" ) {
size.setWidth( cssWidthPropertyToBorderBoxWidth( *width ) );
changed = true;
}
}
if ( getLayoutHeightPolicy() == SizePolicy::Fixed ) {
const auto* height = getUIStyle()->getProperty( PropertyId::Height );
if ( height && height->value() != "auto" ) {
size.setHeight( cssHeightPropertyToBorderBoxHeight( *height ) );
changed = true;
}
}
if ( changed )
setPixelsSize( size );
}
void UIHTMLWidget::setBoxSizing( CSSBoxSizing boxSizing ) {
if ( mBoxSizing != boxSizing ) {
mBoxSizing = boxSizing;
updateCSSContentBoxFixedSize();
notifyLayoutAttrChange( LayoutInvalidation::Self );
notifyLayoutAttrChangeParent( LayoutInvalidation::ParentChildChange );
}
}
void UIHTMLWidget::setVisibility( CSSVisibility val ) { void UIHTMLWidget::setVisibility( CSSVisibility val ) {
if ( mVisibility != val ) { if ( mVisibility != val ) {
mVisibility = val; mVisibility = val;
@@ -597,6 +706,7 @@ void UIHTMLWidget::setJustifySelf( CSSJustifySelf val ) {
std::vector<PropertyId> UIHTMLWidget::getPropertiesImplemented() const { std::vector<PropertyId> UIHTMLWidget::getPropertiesImplemented() const {
auto props = UILayout::getPropertiesImplemented(); auto props = UILayout::getPropertiesImplemented();
auto local = { PropertyId::Display, auto local = { PropertyId::Display,
PropertyId::BoxSizing,
PropertyId::Position, PropertyId::Position,
PropertyId::Float, PropertyId::Float,
PropertyId::Clear, PropertyId::Clear,
@@ -650,6 +760,8 @@ std::string UIHTMLWidget::getPropertyString( const PropertyDefinition* propertyD
switch ( propertyDef->getPropertyId() ) { switch ( propertyDef->getPropertyId() ) {
case PropertyId::Display: case PropertyId::Display:
return CSSDisplayHelper::toString( mDisplay ); return CSSDisplayHelper::toString( mDisplay );
case PropertyId::BoxSizing:
return CSSBoxSizingHelper::toString( mBoxSizing );
case PropertyId::Position: case PropertyId::Position:
return CSSPositionHelper::toString( mPosition ); return CSSPositionHelper::toString( mPosition );
case PropertyId::Float: case PropertyId::Float:
@@ -743,6 +855,10 @@ bool UIHTMLWidget::applyProperty( const StyleSheetProperty& attribute ) {
setDisplay( CSSDisplayHelper::fromString( attribute.asString() ) ); setDisplay( CSSDisplayHelper::fromString( attribute.asString() ) );
return true; return true;
} }
case PropertyId::BoxSizing: {
setBoxSizing( CSSBoxSizingHelper::fromString( attribute.asString() ) );
return true;
}
case PropertyId::Position: { case PropertyId::Position: {
setCSSPosition( CSSPositionHelper::fromString( attribute.asString() ) ); setCSSPosition( CSSPositionHelper::fromString( attribute.asString() ) );
return true; return true;
@@ -765,6 +881,20 @@ bool UIHTMLWidget::applyProperty( const StyleSheetProperty& attribute ) {
mOverflowCreatesBlockFormattingContext = val != "visible"; mOverflowCreatesBlockFormattingContext = val != "visible";
return UILayout::applyProperty( attribute ); return UILayout::applyProperty( attribute );
} }
case PropertyId::Width:
case PropertyId::Height:
case PropertyId::PaddingLeft:
case PropertyId::PaddingRight:
case PropertyId::PaddingTop:
case PropertyId::PaddingBottom:
case PropertyId::BorderLeftWidth:
case PropertyId::BorderRightWidth:
case PropertyId::BorderTopWidth:
case PropertyId::BorderBottomWidth: {
bool applied = UILayout::applyProperty( attribute );
updateCSSContentBoxFixedSize();
return applied;
}
case PropertyId::ZIndex: { case PropertyId::ZIndex: {
setZIndex( attribute.asInt() ); setZIndex( attribute.asInt() );
return true; return true;

View File

@@ -2051,8 +2051,8 @@ void UIRichText::rebuildRichText( UILayout* container, RichText& richText, Intri
const StyleSheetProperty* wprop = const StyleSheetProperty* wprop =
widget->getUIStyle()->getProperty( PropertyId::Width ); widget->getUIStyle()->getProperty( PropertyId::Width );
if ( wprop && StyleSheetLength::isPercentage( wprop->value() ) ) { if ( wprop && StyleSheetLength::isPercentage( wprop->value() ) ) {
widget->setPixelsSize( { widget->lengthFromValue( *wprop ), Float width = widget->cssWidthPropertyToBorderBoxWidth( *wprop );
widget->getPixelsSize().getHeight() } ); widget->setPixelsSize( { width, widget->getPixelsSize().getHeight() } );
} }
} }
if ( widget->getLayoutHeightPolicy() == SizePolicy::Fixed && if ( widget->getLayoutHeightPolicy() == SizePolicy::Fixed &&
@@ -2060,8 +2060,8 @@ void UIRichText::rebuildRichText( UILayout* container, RichText& richText, Intri
const StyleSheetProperty* hprop = const StyleSheetProperty* hprop =
widget->getUIStyle()->getProperty( PropertyId::Height ); widget->getUIStyle()->getProperty( PropertyId::Height );
if ( hprop && StyleSheetLength::isPercentage( hprop->value() ) ) { if ( hprop && StyleSheetLength::isPercentage( hprop->value() ) ) {
widget->setPixelsSize( { widget->getPixelsSize().getWidth(), Float height = widget->cssHeightPropertyToBorderBoxHeight( *hprop );
widget->lengthFromValue( *hprop ) } ); widget->setPixelsSize( { widget->getPixelsSize().getWidth(), height } );
} }
} }
} }

View File

@@ -2907,6 +2907,22 @@ Float UIWidget::getPropertyHeight() const {
return 0.f; return 0.f;
} }
Float UIWidget::cssResolvedLengthToBorderBoxWidth( const Float& resolvedLength ) const {
return resolvedLength;
}
Float UIWidget::cssResolvedLengthToBorderBoxHeight( const Float& resolvedLength ) const {
return resolvedLength;
}
Float UIWidget::cssWidthPropertyToBorderBoxWidth( const StyleSheetProperty& property ) const {
return lengthFromValue( property );
}
Float UIWidget::cssHeightPropertyToBorderBoxHeight( const StyleSheetProperty& property ) const {
return lengthFromValue( property );
}
void UIWidget::setStyleSheetProperties( const CSS::StyleSheetProperties& properties ) { void UIWidget::setStyleSheetProperties( const CSS::StyleSheetProperties& properties ) {
mStyle->setStyleSheetProperties( properties ); mStyle->setStyleSheetProperties( properties );
for ( const auto& [_, property] : properties ) for ( const auto& [_, property] : properties )

View File

@@ -2779,6 +2779,171 @@ UTEST( UIHTML, InlineBlockExplicitWidth ) {
Engine::destroySingleton(); Engine::destroySingleton();
} }
UTEST( UIHTML, FixedBlockWidthUsesContentBoxWithPadding ) {
Engine::instance()->createWindow( WindowSettings( 1024, 653, "Fixed Block Content Box Test",
WindowStyle::Default, WindowBackend::Default,
32, {}, 1, false, true ),
ContextSettings( false, 0, 0, GLv_default, true, false ) );
UISceneNode* sceneNode = init_test_inline_block();
const std::string html = R"html(
<!DOCTYPE html>
<html>
<head>
<style>
html, body { margin: 0; padding: 0; }
#wrapper { width: 300px; padding: 0 20px; }
#borderWrapper { box-sizing: border-box; width: 300px; padding: 0 20px; }
#child, #borderChild { display: block; width: 100%; height: 10px; }
</style>
</head>
<body>
<div id="wrapper">
<div id="child"></div>
</div>
<div id="borderWrapper">
<div id="borderChild"></div>
</div>
</body>
</html>
)html";
sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( html ) );
sceneNode->update( Seconds( 1 ) );
sceneNode->updateDirtyLayouts();
auto* wrapper = sceneNode->getRoot()->find( "wrapper" )->asType<UIWidget>();
auto* child = sceneNode->getRoot()->find( "child" )->asType<UIWidget>();
auto* borderWrapper = sceneNode->getRoot()->find( "borderWrapper" )->asType<UIWidget>();
auto* borderChild = sceneNode->getRoot()->find( "borderChild" )->asType<UIWidget>();
ASSERT_TRUE( wrapper != nullptr );
ASSERT_TRUE( child != nullptr );
ASSERT_TRUE( borderWrapper != nullptr );
ASSERT_TRUE( borderChild != nullptr );
EXPECT_NEAR( wrapper->getPixelsSize().getWidth(), 340.f, 1.f );
EXPECT_NEAR( wrapper->getPixelsSize().getWidth() - wrapper->getPixelsContentOffset().Left -
wrapper->getPixelsContentOffset().Right,
300.f, 1.f );
EXPECT_NEAR( child->getPixelsPosition().x, 20.f, 1.f );
EXPECT_NEAR( child->getPixelsSize().getWidth(), 300.f, 1.f );
EXPECT_NEAR( borderWrapper->getPixelsSize().getWidth(), 300.f, 1.f );
EXPECT_NEAR( borderWrapper->getPixelsSize().getWidth() -
borderWrapper->getPixelsContentOffset().Left -
borderWrapper->getPixelsContentOffset().Right,
260.f, 1.f );
EXPECT_NEAR( borderChild->getPixelsPosition().x, 20.f, 1.f );
EXPECT_NEAR( borderChild->getPixelsSize().getWidth(), 260.f, 1.f );
Engine::destroySingleton();
}
UTEST( UIHTML, BoxSizingAppliesToFlexBasis ) {
Engine::instance()->createWindow( WindowSettings( 1024, 653, "Flex Box Sizing Test",
WindowStyle::Default, WindowBackend::Default,
32, {}, 1, false, true ),
ContextSettings( false, 0, 0, GLv_default, true, false ) );
UISceneNode* sceneNode = init_test_inline_block();
const std::string html = R"html(
<!DOCTYPE html>
<html>
<head>
<style>
html, body { margin: 0; padding: 0; }
#flex { display: flex; width: 800px; }
.item { flex-grow: 0; flex-shrink: 0; flex-basis: 300px; padding: 0 20px; height: 10px; }
#borderItem { box-sizing: border-box; }
</style>
</head>
<body>
<div id="flex">
<div id="contentItem" class="item"></div>
<div id="borderItem" class="item"></div>
</div>
</body>
</html>
)html";
sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( html ) );
sceneNode->update( Seconds( 1 ) );
sceneNode->updateDirtyLayouts();
auto* contentItem = sceneNode->getRoot()->find( "contentItem" )->asType<UIWidget>();
auto* borderItem = sceneNode->getRoot()->find( "borderItem" )->asType<UIWidget>();
ASSERT_TRUE( contentItem != nullptr );
ASSERT_TRUE( borderItem != nullptr );
EXPECT_NEAR( contentItem->getPixelsSize().getWidth(), 340.f, 1.f );
EXPECT_NEAR( borderItem->getPixelsPosition().x, 340.f, 1.f );
EXPECT_NEAR( borderItem->getPixelsSize().getWidth(), 300.f, 1.f );
Engine::destroySingleton();
}
UTEST( UIHTML, BoxSizingAppliesToGridAndTableContainers ) {
Engine::instance()->createWindow( WindowSettings( 1024, 653, "Grid Table Box Sizing Test",
WindowStyle::Default, WindowBackend::Default,
32, {}, 1, false, true ),
ContextSettings( false, 0, 0, GLv_default, true, false ) );
UISceneNode* sceneNode = init_test_inline_block();
const std::string html = R"html(
<!DOCTYPE html>
<html>
<head>
<style>
html, body { margin: 0; padding: 0; }
.grid { display: grid; grid-template-columns: 1fr; width: 300px; padding: 0 20px; }
.table { width: 300px; padding: 0 20px; border-spacing: 0; }
.border { box-sizing: border-box; }
</style>
</head>
<body>
<div id="gridContent" class="grid"><div></div></div>
<div id="gridBorder" class="grid border"><div></div></div>
<table id="tableContent" class="table"><tr><td>x</td></tr></table>
<table id="tableBorder" class="table border"><tr><td>x</td></tr></table>
</body>
</html>
)html";
sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( html ) );
sceneNode->update( Seconds( 1 ) );
sceneNode->updateDirtyLayouts();
auto* gridContent = sceneNode->getRoot()->find( "gridContent" )->asType<UIWidget>();
auto* gridBorder = sceneNode->getRoot()->find( "gridBorder" )->asType<UIWidget>();
auto* tableContent = sceneNode->getRoot()->find( "tableContent" )->asType<UIWidget>();
auto* tableBorder = sceneNode->getRoot()->find( "tableBorder" )->asType<UIWidget>();
ASSERT_TRUE( gridContent != nullptr );
ASSERT_TRUE( gridBorder != nullptr );
ASSERT_TRUE( tableContent != nullptr );
ASSERT_TRUE( tableBorder != nullptr );
EXPECT_NEAR( gridContent->getPixelsSize().getWidth(), 340.f, 1.f );
EXPECT_NEAR( gridBorder->getPixelsSize().getWidth(), 300.f, 1.f );
EXPECT_NEAR( tableContent->getPixelsSize().getWidth(), 300.f, 1.f );
EXPECT_NEAR( tableContent->getPixelsSize().getWidth() -
tableContent->getPixelsContentOffset().Left -
tableContent->getPixelsContentOffset().Right,
260.f, 1.f );
EXPECT_NEAR( tableBorder->getPixelsSize().getWidth(), 300.f, 1.f );
EXPECT_NEAR( tableBorder->getPixelsSize().getWidth() -
tableBorder->getPixelsContentOffset().Left -
tableBorder->getPixelsContentOffset().Right,
260.f, 1.f );
Engine::destroySingleton();
}
UTEST( UIHTML, InlineBlockMixedContent ) { UTEST( UIHTML, InlineBlockMixedContent ) {
Engine::instance()->createWindow( WindowSettings( 1024, 653, "Inline Block Mixed Content Test", Engine::instance()->createWindow( WindowSettings( 1024, 653, "Inline Block Mixed Content Test",
WindowStyle::Default, WindowBackend::Default, WindowStyle::Default, WindowBackend::Default,