From 8141bf9ee3f32fe05eb56565a9138ecd342580b8 Mon Sep 17 00:00:00 2001 From: Damien Date: Thu, 16 Jul 2026 13:20:28 +1100 Subject: [PATCH 1/4] feat(css): parse and store the `aspect-ratio` property Register the `aspect-ratio` longhand, parse the `auto || ` grammar into an ident/pair/list value, and expose it on BoxStyle via an AspectRatio { isAuto, value } accessor. No layout effect yet. Co-Authored-By: Claude Opus 4.8 (1M context) --- source/cssparser.cpp | 37 +++++++++++++++++++++++++++++++++++++ source/cssparser.h | 1 + source/cssproperty.cpp | 1 + source/cssproperty.h | 1 + source/layout/boxstyle.cpp | 34 ++++++++++++++++++++++++++++++++++ source/layout/boxstyle.h | 7 +++++++ 6 files changed, 81 insertions(+) diff --git a/source/cssparser.cpp b/source/cssparser.cpp index a36ca966..86bc7197 100644 --- a/source/cssparser.cpp +++ b/source/cssparser.cpp @@ -3236,6 +3236,8 @@ RefPtr CSSParser::consumeLonghand(CSSTokenStream& input, CSSPropertyID case CSSPropertyID::FlexShrink: case CSSPropertyID::StrokeMiterlimit: return consumeNumber(input, false); + case CSSPropertyID::AspectRatio: + return consumeAspectRatio(input); case CSSPropertyID::TabSize: return consumeLength(input, false, true); case CSSPropertyID::OutlineOffset: @@ -4053,6 +4055,41 @@ RefPtr CSSParser::consumeLonghand(CSSTokenStream& input, CSSPropertyID } } +RefPtr CSSParser::consumeAspectRatio(CSSTokenStream& input) +{ + // aspect-ratio: auto || + // = [ / ]? + auto autoValue = consumeAuto(input); + + RefPtr ratio; + if(auto first = consumeNumber(input, false)) { + RefPtr second; + if(input.consumeSlashIncludingWhitespace()) { + second = consumeNumber(input, false); + if(second == nullptr) + return nullptr; + } else { + second = CSSNumberValue::create(m_heap, 1.0); + } + + ratio = CSSPairValue::create(m_heap, std::move(first), std::move(second)); + } + + if(autoValue == nullptr) + autoValue = consumeAuto(input); + if(autoValue == nullptr && ratio == nullptr) + return nullptr; + if(ratio == nullptr) + return autoValue; + if(autoValue == nullptr) + return ratio; + + CSSValueList values(m_heap); + values.push_back(std::move(autoValue)); + values.push_back(std::move(ratio)); + return CSSListValue::create(m_heap, std::move(values)); +} + bool CSSParser::consumeFlex(CSSTokenStream& input, CSSPropertyList& properties, bool important) { if(consumeIdentIncludingWhitespace(input, "none")) { diff --git a/source/cssparser.h b/source/cssparser.h index 935081a5..bc62f68c 100644 --- a/source/cssparser.h +++ b/source/cssparser.h @@ -101,6 +101,7 @@ class CSSParser { RefPtr consumeWidthOrHeight(CSSTokenStream& input, bool unitless); RefPtr consumeWidthOrHeightOrAuto(CSSTokenStream& input, bool unitless); RefPtr consumeWidthOrHeightOrNone(CSSTokenStream& input, bool unitless); + RefPtr consumeAspectRatio(CSSTokenStream& input); RefPtr consumeString(CSSTokenStream& input); RefPtr consumeCustomIdent(CSSTokenStream& input); diff --git a/source/cssproperty.cpp b/source/cssproperty.cpp index 59b2b643..3dfc96fe 100644 --- a/source/cssproperty.cpp +++ b/source/cssproperty.cpp @@ -35,6 +35,7 @@ CSSPropertyID CSSProperty::id(std::string_view name) {"align-items", CSSPropertyID::AlignItems}, {"align-self", CSSPropertyID::AlignSelf}, {"alignment-baseline", CSSPropertyID::AlignmentBaseline}, + {"aspect-ratio", CSSPropertyID::AspectRatio}, {"background", CSSPropertyID::Background}, {"background-attachment", CSSPropertyID::BackgroundAttachment}, {"background-clip", CSSPropertyID::BackgroundClip}, diff --git a/source/cssproperty.h b/source/cssproperty.h index facc082a..ef557939 100644 --- a/source/cssproperty.h +++ b/source/cssproperty.h @@ -30,6 +30,7 @@ enum class CSSPropertyID : uint16_t { AlignItems, AlignSelf, AlignmentBaseline, + AspectRatio, Background, BackgroundAttachment, BackgroundClip, diff --git a/source/layout/boxstyle.cpp b/source/layout/boxstyle.cpp index 6eb7e94f..931ea049 100644 --- a/source/layout/boxstyle.cpp +++ b/source/layout/boxstyle.cpp @@ -237,6 +237,40 @@ Length BoxStyle::height() const return convertWidthOrHeightLength(*value); } +AspectRatio BoxStyle::aspectRatio() const +{ + auto value = get(CSSPropertyID::AspectRatio); + if(value == nullptr) + return AspectRatio{}; + + const CSSPairValue* pair = nullptr; + bool isAuto = false; + if(is(*value)) { + pair = &to(*value); + } else if(is(*value)) { + for(const auto& item : to(*value)) { + if(is(*item)) + pair = &to(*item); + else if(item->id() == CSSValueID::Auto) { + isAuto = true; + } + } + } else { + isAuto = value->id() == CSSValueID::Auto; + } + + double ratio = 0.0; + if(pair) { + auto width = to(*pair->first()).value(); + auto height = to(*pair->second()).value(); + if(width > 0.0 && height > 0.0) { + ratio = width / height; + } + } + + return AspectRatio{isAuto, ratio}; +} + Length BoxStyle::minWidth() const { auto value = get(CSSPropertyID::MinWidth); diff --git a/source/layout/boxstyle.h b/source/layout/boxstyle.h index 8c46cb17..4c06b853 100644 --- a/source/layout/boxstyle.h +++ b/source/layout/boxstyle.h @@ -453,6 +453,11 @@ inline float Length::calcMin(float maximum) const using LengthList = std::vector; +struct AspectRatio { + bool isAuto = true; + double value = 0.0; // preferred width/height ratio; 0 means none +}; + class LengthPoint { public: explicit LengthPoint(const Length& value) : LengthPoint(value, value) {} @@ -789,6 +794,8 @@ class BoxStyle : public HeapMember, public RefCounted { ObjectFit objectFit() const { return m_objectFit; } LengthPoint objectPosition() const; + AspectRatio aspectRatio() const; + TableLayout tableLayout() const { return m_tableLayout; } CaptionSide captionSide() const { return m_captionSide; } EmptyCells emptyCells() const { return m_emptyCells; } From bb15b071df84e29fb4bee85d6d84e834acc5cd60 Mon Sep 17 00:00:00 2001 From: Damien Date: Thu, 16 Jul 2026 13:24:27 +1100 Subject: [PATCH 2/4] feat(layout): apply `aspect-ratio` to replaced boxes Blend the CSS aspect-ratio into ReplacedBox::computeAspectRatioInformation: a specified ratio overrides the natural one, unless `auto` was given and the element has a natural ratio of its own (per CSS Sizing 4). Co-Authored-By: Claude Opus 4.8 (1M context) --- source/layout/replacedbox.cpp | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/source/layout/replacedbox.cpp b/source/layout/replacedbox.cpp index c6c4246e..aaa70455 100644 --- a/source/layout/replacedbox.cpp +++ b/source/layout/replacedbox.cpp @@ -22,6 +22,14 @@ ReplacedBox::ReplacedBox(Node* node, const RefPtr& style) void ReplacedBox::computeAspectRatioInformation(float& intrinsicWidth, float& intrinsicHeight, double& intrinsicRatio) const { computeIntrinsicRatioInformation(intrinsicWidth, intrinsicHeight, intrinsicRatio); + + // A CSS 'aspect-ratio' overrides the natural ratio unless 'auto' was + // specified and the element actually has a natural ratio of its own. + auto aspectRatio = style()->aspectRatio(); + if(aspectRatio.value > 0.0 && (!aspectRatio.isAuto || intrinsicRatio == 0.0)) { + intrinsicRatio = aspectRatio.value; + } + if(intrinsicRatio && intrinsicWidth && intrinsicHeight && style()->height().isAuto() && style()->width().isAuto()) { auto constrainedWidth = constrainReplacedWidth(intrinsicWidth); auto constrainedHeight = constrainReplacedHeight(intrinsicHeight); From b030faf130244f09f78742be782eb6aedb92a366 Mon Sep 17 00:00:00 2001 From: Damien Date: Thu, 16 Jul 2026 13:24:27 +1100 Subject: [PATCH 3/4] feat(layout): apply `aspect-ratio` to non-replaced block boxes Derive the automatic axis from the definite one in BlockBox::computeWidth /computeHeight. The ratio applies to the content box, except for a bare `` where it applies to the box designated by `box-sizing`. Co-Authored-By: Claude Opus 4.8 (1M context) --- source/layout/blockbox.cpp | 36 +++++++++++++++++++++++++++++++++++- source/layout/blockbox.h | 3 +++ 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/source/layout/blockbox.cpp b/source/layout/blockbox.cpp index eb3a754c..adb767dd 100644 --- a/source/layout/blockbox.cpp +++ b/source/layout/blockbox.cpp @@ -166,6 +166,28 @@ std::optional BlockBox::computeHeightUsing(const Length& heightLength) co return std::nullopt; } +float BlockBox::computeBorderBoxHeightUsingAspectRatio(const AspectRatio& aspectRatio) const +{ + // The ratio applies to the content box, except for a bare '' + // (not 'auto'), where it applies to the box designated by 'box-sizing'. + if(aspectRatio.isAuto || style()->boxSizing() == BoxSizing::ContentBox) { + auto contentWidth = std::max(0.f, width() - borderAndPaddingWidth()); + return contentWidth / aspectRatio.value + borderAndPaddingHeight(); + } + + return width() / aspectRatio.value; +} + +float BlockBox::computeBorderBoxWidthUsingAspectRatio(const AspectRatio& aspectRatio, float borderBoxHeight) const +{ + if(aspectRatio.isAuto || style()->boxSizing() == BoxSizing::ContentBox) { + auto contentHeight = std::max(0.f, borderBoxHeight - borderAndPaddingHeight()); + return contentHeight * aspectRatio.value + borderAndPaddingWidth(); + } + + return borderBoxHeight * aspectRatio.value; +} + float BlockBox::constrainWidth(float width, const BlockBox* container, float containerWidth) const { auto minWidthLength = style()->minWidth(); @@ -463,6 +485,14 @@ void BlockBox::computeWidth(float& x, float& width, float& marginLeft, float& ma auto container = containingBlock(); auto containerWidth = std::max(0.f, containingBlockWidthForContent(container)); width = computeWidthUsing(style()->width(), container, containerWidth); + if(style()->width().isAuto()) { + if(auto aspectRatio = style()->aspectRatio(); aspectRatio.value > 0.f) { + if(auto computedHeight = computeHeightUsing(style()->height())) { + width = computeBorderBoxWidthUsingAspectRatio(aspectRatio, adjustBorderBoxHeight(computedHeight.value())); + } + } + } + width = constrainWidth(width, container, containerWidth); if(isTableBox()) width = std::max(width, minPreferredWidth()); @@ -489,8 +519,12 @@ void BlockBox::computeHeight(float& y, float& height, float& marginTop, float& m computeVerticalMargins(marginTop, marginBottom); if(isTableBox()) return; - if(auto computedHeight = computeHeightUsing(style()->height())) + if(auto computedHeight = computeHeightUsing(style()->height())) { height = adjustBorderBoxHeight(computedHeight.value()); + } else if(auto aspectRatio = style()->aspectRatio(); aspectRatio.value > 0.f) { + height = computeBorderBoxHeightUsingAspectRatio(aspectRatio); + } + height = constrainBorderBoxHeight(height); } diff --git a/source/layout/blockbox.h b/source/layout/blockbox.h index f70464dc..322087c4 100644 --- a/source/layout/blockbox.h +++ b/source/layout/blockbox.h @@ -49,6 +49,9 @@ class BlockBox : public BoxFrame { float computeWidthUsing(const Length& widthLength, const BlockBox* container, float containerWidth) const; std::optional computeHeightUsing(const Length& heightLength) const; + float computeBorderBoxHeightUsingAspectRatio(const AspectRatio& aspectRatio) const; + float computeBorderBoxWidthUsingAspectRatio(const AspectRatio& aspectRatio, float borderBoxHeight) const; + float constrainWidth(float width, const BlockBox* container, float containerWidth) const; float constrainBorderBoxHeight(float height) const; float constrainContentBoxHeight(float height) const; From 304c31e17131f1e0ec9fccc9472c988fddae2fcb Mon Sep 17 00:00:00 2001 From: Damien Date: Thu, 16 Jul 2026 13:24:53 +1100 Subject: [PATCH 4/4] docs: document `aspect-ratio` support Co-Authored-By: Claude Opus 4.8 (1M context) --- CHANGELOG.md | 4 ++++ FEATURES.md | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 20b62b44..50e8bc7d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## Unreleased + +- Add support for the CSS `aspect-ratio` property + ## PlutoBook 0.19.0 (2026-08-04) - Fix several security vulnerabilities diff --git a/FEATURES.md b/FEATURES.md index 7a641202..54abb54c 100644 --- a/FEATURES.md +++ b/FEATURES.md @@ -50,7 +50,7 @@ PlutoBook implements the CSS Box Model as described in the [CSS Box Model Module ## Box Sizing -PlutoBook supports box sizing properties as described in the [CSS Box Sizing Module Level 3](https://www.w3.org/TR/css-sizing-3). It provides control over element dimensions using `width`, `height`, `min-width`, `min-height`, `max-width`, and `max-height`. These properties make it easy to define fixed, minimum, or maximum sizes to ensure flexible and predictable layouts. The `box-sizing` property is also supported, allowing you to choose whether an element’s `width` and `height` include its padding and border (`border-box`) or apply only to its content (`content-box`). This gives precise control over how an element’s total size is calculated in relation to its content and surrounding box model properties. +PlutoBook supports box sizing properties as described in the [CSS Box Sizing Module Level 3](https://www.w3.org/TR/css-sizing-3). It provides control over element dimensions using `width`, `height`, `min-width`, `min-height`, `max-width`, and `max-height`. These properties make it easy to define fixed, minimum, or maximum sizes to ensure flexible and predictable layouts. The `box-sizing` property is also supported, allowing you to choose whether an element’s `width` and `height` include its padding and border (`border-box`) or apply only to its content (`content-box`). This gives precise control over how an element’s total size is calculated in relation to its content and surrounding box model properties. The `aspect-ratio` property is also supported, letting an element’s automatic dimension be derived from a preferred width-to-height ratio and the other, definite dimension. ## Display