margin: auto support.

background from body to html when html background is transparent.
This commit is contained in:
Martín Lucas Golini
2026-04-14 00:54:47 -03:00
parent e2b9b47b21
commit 16579dad0e
9 changed files with 449 additions and 21 deletions

View File

@@ -125,6 +125,8 @@ enum UINodeType {
UI_TYPE_DROPDOWNMODELLIST,
UI_TYPE_DIFF_VIEW,
UI_TYPE_BR,
UI_TYPE_HTML_HTML,
UI_TYPE_HTML_BODY,
UI_TYPE_MODULES = 10000,
UI_TYPE_TERMINAL = 10001,
UI_TYPE_USER = 200000,

View File

@@ -30,6 +30,10 @@ class EE_API UIRichText : public UILayout {
static UIRichText* NewHr();
static UIRichText* NewHtml();
static UIRichText* NewBody();
static UIRichText* NewDiv() { return UIRichText::NewWithTag( "div" ); };
static UIRichText* NewPre() { return UIRichText::NewWithTag( "pre" ); };
@@ -157,6 +161,29 @@ class EE_API UIRichText : public UILayout {
void updateDefaultSpansStyle();
};
class EE_API UIHTMLHtml : public UIRichText {
public:
static UIHTMLHtml* New( const std::string& tag );
virtual Uint32 getType() const override;
bool isType( const Uint32& type ) const override;
protected:
UIHTMLHtml( const std::string& tag = "html" );
};
class EE_API UIHTMLBody : public UIRichText {
public:
static UIHTMLBody* New( const std::string& tag );
virtual Uint32 getType() const override;
bool isType( const Uint32& type ) const override;
bool applyProperty( const StyleSheetProperty& attribute ) override;
protected:
bool mPropagatedBackground{ false };
UIHTMLBody( const std::string& tag = "body" );
};
}} // namespace EE::UI
#endif

View File

@@ -303,6 +303,24 @@ class EE_API UIWidget : public UINode {
*/
UIWidget* setLayoutMarginBottom( const Float& marginBottom );
UIWidget* setLayoutMarginLeftAuto( bool isAuto );
UIWidget* setLayoutMarginRightAuto( bool isAuto );
UIWidget* setLayoutMarginTopAuto( bool isAuto );
UIWidget* setLayoutMarginBottomAuto( bool isAuto );
UIWidget* setLayoutMarginAuto( bool left, bool right, bool top, bool bottom );
bool hasLayoutMarginLeftAuto() const;
bool hasLayoutMarginRightAuto() const;
bool hasLayoutMarginTopAuto() const;
bool hasLayoutMarginBottomAuto() const;
/**
* @brief Sets the layout margin for all sides in pixels.
*
@@ -1330,6 +1348,14 @@ class EE_API UIWidget : public UINode {
mutable Float mMinIntrinsicWidth{ 0 };
mutable Float mMaxIntrinsicWidth{ 0 };
mutable bool mIntrinsicWidthsDirty{ true };
Uint8 mMarginAuto{ 0 };
static constexpr Uint8 MarginAutoLeft = ( 1 << 0 );
static constexpr Uint8 MarginAutoRight = ( 1 << 1 );
static constexpr Uint8 MarginAutoTop = ( 1 << 2 );
static constexpr Uint8 MarginAutoBottom = ( 1 << 3 );
void calculateAutoMargin();
/**
* @brief Default constructor.
@@ -1680,6 +1706,8 @@ class EE_API UIWidget : public UINode {
/* @return The size of the widget when size policy is match_parent */
Sizef getSizeFromLayoutPolicy();
UIWidget* setLayoutMarginAuto( Uint32 dir, bool isAuto );
};
}} // namespace EE::UI

View File

@@ -28,6 +28,72 @@ class UILineBreak : public UIRichText {
}
};
UIHTMLHtml* UIHTMLHtml::New( const std::string& tag ) {
return eeNew( UIHTMLHtml, ( tag ) );
}
UIHTMLHtml::UIHTMLHtml( const std::string& tag ) : UIRichText( tag ) {}
Uint32 UIHTMLHtml::getType() const {
return UI_TYPE_HTML_HTML;
}
bool UIHTMLHtml::isType( const Uint32& type ) const {
return UIHTMLHtml::getType() == type ? true : UIRichText::isType( type );
}
UIHTMLBody* UIHTMLBody::New( const std::string& tag ) {
return eeNew( UIHTMLBody, ( tag ) );
}
UIHTMLBody::UIHTMLBody( const std::string& tag ) : UIRichText( tag ) {}
Uint32 UIHTMLBody::getType() const {
return UI_TYPE_HTML_BODY;
}
bool UIHTMLBody::isType( const Uint32& type ) const {
return UIHTMLBody::getType() == type ? true : UIRichText::isType( type );
}
bool UIHTMLBody::applyProperty( const StyleSheetProperty& attribute ) {
if ( !checkPropertyDefinition( attribute ) )
return false;
switch ( attribute.getPropertyDefinition()->getPropertyId() ) {
case PropertyId::BackgroundColor:
case PropertyId::BackgroundImage:
case PropertyId::BackgroundTint:
case PropertyId::BackgroundPositionX:
case PropertyId::BackgroundPositionY:
case PropertyId::BackgroundRepeat:
case PropertyId::BackgroundSize: {
if ( getParent() && getParent()->isType( UI_TYPE_HTML_HTML ) ) {
UIWidget* htmlParent = getParent()->asType<UIWidget>();
if ( htmlParent->getBackgroundColor() == Color::Transparent ||
mPropagatedBackground ) {
mPropagatedBackground = true;
htmlParent->applyProperty( attribute );
return true;
}
}
break;
}
default:
break;
}
return UIRichText::applyProperty( attribute );
}
UIRichText* UIRichText::NewHtml() {
return UIHTMLHtml::New( "html" );
}
UIRichText* UIRichText::NewBody() {
return UIHTMLBody::New( "body" );
}
UIRichText* UIRichText::NewBr() {
return UILineBreak::New( "br" );
};
@@ -554,7 +620,7 @@ void UIRichText::rebuildRichText( RichText& richText, IntrinsicMode mode ) {
richText.addCustomSize( Sizef( w + margin.Left + margin.Right,
size.getHeight() + margin.Top + margin.Bottom ),
isBlock );
isBlock );
}
};

View File

@@ -156,6 +156,60 @@ UIWidget* UIWidget::setLayoutMarginBottom( const Float& marginBottom ) {
return this;
}
UIWidget* UIWidget::setLayoutMarginAuto( Uint32 dir, bool isAuto ) {
if ( isAuto != ( ( mMarginAuto & dir ) != 0 ) ) {
if ( isAuto ) {
mMarginAuto |= dir;
calculateAutoMargin();
} else {
mMarginAuto &= ~dir;
notifyLayoutAttrChange();
notifyLayoutAttrChangeParent();
}
}
return this;
}
UIWidget* UIWidget::setLayoutMarginLeftAuto( bool isAuto ) {
return setLayoutMarginAuto( MarginAutoLeft, isAuto );
}
UIWidget* UIWidget::setLayoutMarginRightAuto( bool isAuto ) {
return setLayoutMarginAuto( MarginAutoRight, isAuto );
}
UIWidget* UIWidget::setLayoutMarginTopAuto( bool isAuto ) {
return setLayoutMarginAuto( MarginAutoTop, isAuto );
}
UIWidget* UIWidget::setLayoutMarginBottomAuto( bool isAuto ) {
return setLayoutMarginAuto( MarginAutoTop, isAuto );
}
UIWidget* UIWidget::setLayoutMarginAuto( bool left, bool right, bool top, bool bottom ) {
setLayoutMarginLeftAuto( left );
setLayoutMarginRightAuto( right );
setLayoutMarginTopAuto( top );
setLayoutMarginBottomAuto( bottom );
return this;
}
bool UIWidget::hasLayoutMarginLeftAuto() const {
return mMarginAuto & MarginAutoLeft;
}
bool UIWidget::hasLayoutMarginRightAuto() const {
return mMarginAuto & MarginAutoRight;
}
bool UIWidget::hasLayoutMarginTopAuto() const {
return mMarginAuto & MarginAutoTop;
}
bool UIWidget::hasLayoutMarginBottomAuto() const {
return mMarginAuto & MarginAutoBottom;
}
UIWidget* UIWidget::setLayoutPixelsMargin( const Rectf& margin ) {
if ( mLayoutMargin != margin ) {
mLayoutMarginPx = margin;
@@ -533,8 +587,83 @@ UITooltip* UIWidget::getTooltip() {
return mTooltip;
}
void UIWidget::calculateAutoMargin() {
if ( !mMarginAuto || !getParent() || !getParent()->isWidget() )
return;
UIWidget* parent = getParent()->asType<UIWidget>();
Sizef parentSize = parent->getPixelsSize();
Rectf parentPadding = parent->getPixelsPadding();
bool changed = false;
if ( ( mMarginAuto & MarginAutoLeft ) && ( mMarginAuto & MarginAutoRight ) ) {
Float availableWidth = parentSize.getWidth() - parentPadding.Left - parentPadding.Right -
getPixelsSize().getWidth();
Float newMarginLeft = availableWidth > 0 ? availableWidth / 2.f : 0.f;
Float newMarginRight = availableWidth > 0 ? availableWidth / 2.f : 0.f;
if ( mLayoutMarginPx.Left != newMarginLeft || mLayoutMarginPx.Right != newMarginRight ) {
mLayoutMarginPx.Left = newMarginLeft;
mLayoutMarginPx.Right = newMarginRight;
changed = true;
}
} else if ( mMarginAuto & MarginAutoLeft ) {
Float availableWidth = parentSize.getWidth() - parentPadding.Left - parentPadding.Right -
getPixelsSize().getWidth() - mLayoutMarginPx.Right;
Float newMarginLeft = std::max( 0.f, availableWidth );
if ( mLayoutMarginPx.Left != newMarginLeft ) {
mLayoutMarginPx.Left = newMarginLeft;
changed = true;
}
} else if ( mMarginAuto & MarginAutoRight ) {
Float availableWidth = parentSize.getWidth() - parentPadding.Left - parentPadding.Right -
getPixelsSize().getWidth() - mLayoutMarginPx.Left;
Float newMarginRight = std::max( 0.f, availableWidth );
if ( mLayoutMarginPx.Right != newMarginRight ) {
mLayoutMarginPx.Right = newMarginRight;
changed = true;
}
}
if ( ( mMarginAuto & MarginAutoTop ) && ( mMarginAuto & MarginAutoBottom ) ) {
Float availableHeight = parentSize.getHeight() - parentPadding.Top - parentPadding.Bottom -
getPixelsSize().getHeight();
Float newMarginTop = availableHeight > 0 ? availableHeight / 2.f : 0.f;
Float newMarginBottom = availableHeight > 0 ? availableHeight / 2.f : 0.f;
if ( mLayoutMarginPx.Top != newMarginTop || mLayoutMarginPx.Bottom != newMarginBottom ) {
mLayoutMarginPx.Top = newMarginTop;
mLayoutMarginPx.Bottom = newMarginBottom;
changed = true;
}
} else if ( mMarginAuto & MarginAutoTop ) {
Float availableHeight = parentSize.getHeight() - parentPadding.Top - parentPadding.Bottom -
getPixelsSize().getHeight() - mLayoutMarginPx.Bottom;
Float newMarginTop = std::max( 0.f, availableHeight );
if ( mLayoutMarginPx.Top != newMarginTop ) {
mLayoutMarginPx.Top = newMarginTop;
changed = true;
}
} else if ( mMarginAuto & MarginAutoBottom ) {
Float availableHeight = parentSize.getHeight() - parentPadding.Top - parentPadding.Bottom -
getPixelsSize().getHeight() - mLayoutMarginPx.Top;
Float newMarginBottom = std::max( 0.f, availableHeight );
if ( mLayoutMarginPx.Bottom != newMarginBottom ) {
mLayoutMarginPx.Bottom = newMarginBottom;
changed = true;
}
}
if ( changed ) {
mLayoutMargin = PixelDensity::pxToDp( mLayoutMarginPx );
onMarginChange();
notifyLayoutAttrChange();
notifyLayoutAttrChangeParent();
}
}
void UIWidget::onParentSizeChange( const Vector2f& sizeChange ) {
updateAnchors( sizeChange );
if ( mMarginAuto != 0 )
calculateAutoMargin();
UINode::onParentSizeChange( sizeChange );
}
@@ -551,6 +680,8 @@ void UIWidget::onVisibilityChange() {
}
void UIWidget::onSizeChange() {
if ( mMarginAuto != 0 )
calculateAutoMargin();
UINode::onSizeChange();
if ( mBorder != NULL )
@@ -1845,18 +1976,46 @@ bool UIWidget::applyProperty( const StyleSheetProperty& attribute ) {
}
break;
}
case PropertyId::MarginLeft:
setLayoutMarginLeft( lengthFromValueAsDp( attribute ) );
case PropertyId::MarginLeft: {
if ( attribute.asString() == "auto" ) {
mMarginAuto |= MarginAutoLeft;
calculateAutoMargin();
} else {
mMarginAuto &= ~MarginAutoLeft;
setLayoutMarginLeft( lengthFromValueAsDp( attribute ) );
}
break;
case PropertyId::MarginRight:
setLayoutMarginRight( lengthFromValueAsDp( attribute ) );
}
case PropertyId::MarginRight: {
if ( attribute.asString() == "auto" ) {
mMarginAuto |= MarginAutoRight;
calculateAutoMargin();
} else {
mMarginAuto &= ~MarginAutoRight;
setLayoutMarginRight( lengthFromValueAsDp( attribute ) );
}
break;
case PropertyId::MarginTop:
setLayoutMarginTop( lengthFromValueAsDp( attribute ) );
}
case PropertyId::MarginTop: {
if ( attribute.asString() == "auto" ) {
mMarginAuto |= MarginAutoTop;
calculateAutoMargin();
} else {
mMarginAuto &= ~MarginAutoTop;
setLayoutMarginTop( lengthFromValueAsDp( attribute ) );
}
break;
case PropertyId::MarginBottom:
setLayoutMarginBottom( lengthFromValueAsDp( attribute ) );
}
case PropertyId::MarginBottom: {
if ( attribute.asString() == "auto" ) {
mMarginAuto |= MarginAutoBottom;
calculateAutoMargin();
} else {
mMarginAuto &= ~MarginAutoBottom;
setLayoutMarginBottom( lengthFromValueAsDp( attribute ) );
}
break;
}
case PropertyId::Tooltip: {
String text = getTranslatorString( attribute.value() );
setTooltipText( text );
@@ -2410,12 +2569,14 @@ Float UIWidget::getMatchParentWidth() const {
if ( getParent()->isWidget() )
padding = static_cast<UIWidget*>( getParent() )->getPixelsPadding();
Float width = getParent()->getPixelsSize().getWidth() - mLayoutMarginPx.Left -
mLayoutMarginPx.Right - padding.Left - padding.Right;
Float marginLeft = ( mMarginAuto & MarginAutoLeft ) ? 0.f : mLayoutMarginPx.Left;
Float marginRight = ( mMarginAuto & MarginAutoRight ) ? 0.f : mLayoutMarginPx.Right;
Float width = getParent()->getPixelsSize().getWidth() - marginLeft - marginRight -
padding.Left - padding.Right;
if ( !mMaxWidthEq.empty() ) {
Float maxWidth( getMaxSizePx().getWidth() - mLayoutMarginPx.Left - mLayoutMarginPx.Right -
padding.Left - padding.Right );
Float maxWidth( getMaxSizePx().getWidth() );
if ( maxWidth > 0 && maxWidth < width )
width = maxWidth;
}
@@ -2427,14 +2588,16 @@ Float UIWidget::getMatchParentHeight() const {
Rectf padding = Rectf::Zero;
if ( getParent()->isWidget() )
padding = static_cast<UIWidget*>( getParent() )->getPadding();
padding = static_cast<UIWidget*>( getParent() )->getPixelsPadding();
Float height = getParent()->getPixelsSize().getHeight() - mLayoutMarginPx.Top -
mLayoutMarginPx.Bottom - padding.Top - padding.Bottom;
Float marginTop = ( mMarginAuto & MarginAutoTop ) ? 0.f : mLayoutMarginPx.Top;
Float marginBottom = ( mMarginAuto & MarginAutoBottom ) ? 0.f : mLayoutMarginPx.Bottom;
Float height = getParent()->getPixelsSize().getHeight() - marginTop - marginBottom -
padding.Top - padding.Bottom;
if ( !mMaxHeightEq.empty() ) {
Float maxHeight( getMaxSizePx().getHeight() - mLayoutMarginPx.Left - mLayoutMarginPx.Right -
padding.Left - padding.Right );
Float maxHeight( getMaxSizePx().getHeight() );
if ( maxHeight > 0 && maxHeight < height )
height = maxHeight;
}

View File

@@ -173,9 +173,9 @@ void UIWidgetCreator::createBaseWidgetList() {
registeredWidget["section"] = [] { return UIRichText::NewWithTag( "section" ); };
registeredWidget["nav"] = [] { return UIRichText::NewWithTag( "nav" ); };
registeredWidget["center"] = [] { return UIRichText::NewWithTag( "center" ); };
registeredWidget["html"] = [] { return UIRichText::NewWithTag( "html" ); };
registeredWidget["html"] = UIRichText::NewHtml;
registeredWidget["head"] = [] { return UIWidget::NewWithTag( "head" ); };
registeredWidget["body"] = [] { return UIRichText::NewWithTag( "body" ); };
registeredWidget["body"] = UIRichText::NewBody;
registeredWidget["form"] = [] { return UIRichText::NewWithTag( "form" ); };
registeredWidget["table"] = UIHTMLTable::New;
registeredWidget["tr"] = UIHTMLTableRow::New;

View File

@@ -7,6 +7,7 @@
#include <eepp/scene/scenemanager.hpp>
#include <eepp/system/filesystem.hpp>
#include <eepp/system/sys.hpp>
#include <eepp/ui/css/stylesheetparser.hpp>
#include <eepp/ui/htmlinput.hpp>
#include <eepp/ui/htmltextarea.hpp>
#include <eepp/ui/htmltextinput.hpp>
@@ -429,3 +430,139 @@ UTEST( UIHTMLTable, tableLayoutFixed ) {
Engine::destroySingleton();
}
UTEST( UIHTMLBody, backgroundColorPropagation ) {
Engine::instance()->createWindow( WindowSettings( 1024, 650, "HTML Tables Test",
WindowStyle::Default, WindowBackend::Default,
32, {}, 1, false, true ) );
FileSystem::changeWorkingDirectory( Sys::getProcessPath() );
FontTrueType* font = FontTrueType::New( "NotoSans-Regular" );
font->loadFromFile( "../assets/fonts/NotoSans-Regular.ttf" );
ASSERT_TRUE( font != nullptr && font->loaded() );
FontFamily::loadFromRegular( font );
UI::UISceneNode* sceneNode = UI::UISceneNode::New();
SceneManager::instance()->add( sceneNode );
UI::UIThemeManager* themeManager = sceneNode->getUIThemeManager();
themeManager->setDefaultFont( font );
sceneNode->loadLayoutFromString(
R"(<html id="html_el">
<body id="body_el" style="background-color: red; max-width: 960px;">
</body>
</html>)" );
sceneNode->updateDirtyLayouts();
auto html_el = sceneNode->getRoot()->find( "html_el" );
auto body_el = sceneNode->getRoot()->find( "body_el" );
ASSERT_TRUE( html_el != nullptr );
ASSERT_TRUE( body_el != nullptr );
// HTML element should have inherited the red background color, and body should be transparent
EXPECT_TRUE( html_el->asType<UIWidget>()->getBackgroundColor() == Color::Red );
EXPECT_TRUE( body_el->asType<UIWidget>()->getBackgroundColor() == Color::Transparent );
Engine::destroySingleton();
}
UTEST( UIHTMLBody, maxWidthResizingBug ) {
Engine::instance()->createWindow( WindowSettings( 1024, 768, "HTML Resize Bug",
WindowStyle::Default, WindowBackend::Default,
32, {}, 1, false, true ) );
FileSystem::changeWorkingDirectory( Sys::getProcessPath() );
UI::UISceneNode* sceneNode = UI::UISceneNode::New();
SceneManager::instance()->add( sceneNode );
UI::CSS::StyleSheetParser parser;
parser.loadFromFile( "/tmp/style.css" );
sceneNode->setStyleSheet( parser.getStyleSheet() );
std::string htmlContent;
FileSystem::fileGet( "/tmp/dwarmstrong.html", htmlContent );
sceneNode->loadLayoutFromString( htmlContent );
sceneNode->getRoot()->setSize( 1024, 768 );
sceneNode->updateDirtyLayouts();
auto body_el = sceneNode->getRoot()->findByType( UI_TYPE_HTML_BODY )->asType<UIWidget>();
ASSERT_TRUE( body_el != nullptr );
Float widthAt1024 = body_el->getPixelsSize().getWidth();
EXPECT_NEAR( widthAt1024, 960.f, 10.f ); // It should be around 960px (minus some margins if any)
sceneNode->getRoot()->setSize( 2048, 768 );
sceneNode->updateDirtyLayouts();
Float widthAt2048 = body_el->getPixelsSize().getWidth();
EXPECT_NEAR( widthAt2048, 960.f, 10.f ); // Body should stay 960px even when parent is huge
sceneNode->getRoot()->setSize( 1024, 768 );
sceneNode->updateDirtyLayouts();
Float widthAfterResize = body_el->getPixelsSize().getWidth();
EXPECT_NEAR( widthAt1024, widthAfterResize, 1.f );
Engine::destroySingleton();
}
UTEST( UILayout, marginAuto ) {
Engine::instance()->createWindow( WindowSettings( 1024, 650, "Margin Auto Test",
WindowStyle::Default, WindowBackend::Default,
32, {}, 1, false, true ) );
FileSystem::changeWorkingDirectory( Sys::getProcessPath() );
FontTrueType* font = FontTrueType::New( "NotoSans-Regular" );
font->loadFromFile( "../assets/fonts/NotoSans-Regular.ttf" );
ASSERT_TRUE( font != nullptr && font->loaded() );
FontFamily::loadFromRegular( font );
UI::UISceneNode* sceneNode = UI::UISceneNode::New();
SceneManager::instance()->add( sceneNode );
UI::UIThemeManager* themeManager = sceneNode->getUIThemeManager();
themeManager->setDefaultFont( font );
auto* container = sceneNode->loadLayoutFromString(
R"(<vbox id="container">
<widget id="child" style="margin: 0 auto;" />
</vbox>)" );
auto child = sceneNode->getRoot()->find( "child" );
ASSERT_TRUE( child != nullptr );
UIWidget* childWidget = child->asType<UIWidget>();
UIWidget* contWidget = container->asType<UIWidget>();
contWidget->setSize( 500, 500 );
childWidget->setSize( 100, 100 );
sceneNode->updateDirtyLayouts();
Float expectedMarginX = ( contWidget->getPixelsSize().getWidth() - childWidget->getPixelsSize().getWidth() ) / 2.f;
// Margin left/right should be auto computed to expectedMarginX
EXPECT_NEAR( childWidget->getLayoutPixelsMargin().Left, expectedMarginX, 1.f );
EXPECT_NEAR( childWidget->getLayoutPixelsMargin().Right, expectedMarginX, 1.f );
EXPECT_NEAR( childWidget->getLayoutPixelsMargin().Top, 0.f, 1.f );
EXPECT_NEAR( childWidget->getLayoutPixelsMargin().Bottom, 0.f, 1.f );
// Resize parent and see if margins re-evaluate automatically
contWidget->setSize( 800, 800 );
sceneNode->updateDirtyLayouts();
expectedMarginX = ( contWidget->getPixelsSize().getWidth() - childWidget->getPixelsSize().getWidth() ) / 2.f;
EXPECT_NEAR( childWidget->getLayoutPixelsMargin().Left, expectedMarginX, 1.f );
EXPECT_NEAR( childWidget->getLayoutPixelsMargin().Right, expectedMarginX, 1.f );
// Now test resize of child
childWidget->setSize( 200, 100 );
sceneNode->updateDirtyLayouts();
expectedMarginX = ( contWidget->getPixelsSize().getWidth() - childWidget->getPixelsSize().getWidth() ) / 2.f;
EXPECT_NEAR( childWidget->getLayoutPixelsMargin().Left, expectedMarginX, 1.f );
EXPECT_NEAR( childWidget->getLayoutPixelsMargin().Right, expectedMarginX, 1.f );
Engine::destroySingleton();
}

View File

@@ -3095,7 +3095,7 @@ void LLMChatUI::updateTabTitle() {
}
if ( hasPendingPermissions )
title += "- ✋ " + i18n( "action_required", "Action Required" ) + " - " + title;
title = " - ✋ " + i18n( "action_required", "Action Required" ) + " - " + title;
if ( !mSummary.empty() )
title += " - " + mSummary;

View File

@@ -0,0 +1,5 @@
<html>
<body style="max-width: 960px; margin: 0 auto; background-color: red;">
<div style="width: 100px; height: 100px; background-color: blue;"></div>
</body>
</html>