Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
2 changes: 1 addition & 1 deletion FEATURES.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
37 changes: 37 additions & 0 deletions source/cssparser.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3236,6 +3236,8 @@ RefPtr<CSSValue> 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:
Expand Down Expand Up @@ -4053,6 +4055,41 @@ RefPtr<CSSValue> CSSParser::consumeLonghand(CSSTokenStream& input, CSSPropertyID
}
}

RefPtr<CSSValue> CSSParser::consumeAspectRatio(CSSTokenStream& input)
{
// aspect-ratio: auto || <ratio>
// <ratio> = <number [0,inf]> [ / <number [0,inf]> ]?
auto autoValue = consumeAuto(input);

RefPtr<CSSValue> ratio;
if(auto first = consumeNumber(input, false)) {
RefPtr<CSSValue> 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")) {
Expand Down
1 change: 1 addition & 0 deletions source/cssparser.h
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ class CSSParser {
RefPtr<CSSValue> consumeWidthOrHeight(CSSTokenStream& input, bool unitless);
RefPtr<CSSValue> consumeWidthOrHeightOrAuto(CSSTokenStream& input, bool unitless);
RefPtr<CSSValue> consumeWidthOrHeightOrNone(CSSTokenStream& input, bool unitless);
RefPtr<CSSValue> consumeAspectRatio(CSSTokenStream& input);

RefPtr<CSSValue> consumeString(CSSTokenStream& input);
RefPtr<CSSValue> consumeCustomIdent(CSSTokenStream& input);
Expand Down
1 change: 1 addition & 0 deletions source/cssproperty.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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},
Expand Down
1 change: 1 addition & 0 deletions source/cssproperty.h
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ enum class CSSPropertyID : uint16_t {
AlignItems,
AlignSelf,
AlignmentBaseline,
AspectRatio,
Background,
BackgroundAttachment,
BackgroundClip,
Expand Down
36 changes: 35 additions & 1 deletion source/layout/blockbox.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,28 @@ std::optional<float> 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 '<ratio>'
// (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();
Expand Down Expand Up @@ -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());
Expand All @@ -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);
}

Expand Down
3 changes: 3 additions & 0 deletions source/layout/blockbox.h
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,9 @@ class BlockBox : public BoxFrame {
float computeWidthUsing(const Length& widthLength, const BlockBox* container, float containerWidth) const;
std::optional<float> 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;
Expand Down
34 changes: 34 additions & 0 deletions source/layout/boxstyle.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<CSSPairValue>(*value)) {
pair = &to<CSSPairValue>(*value);
} else if(is<CSSListValue>(*value)) {
for(const auto& item : to<CSSListValue>(*value)) {
if(is<CSSPairValue>(*item))
pair = &to<CSSPairValue>(*item);
else if(item->id() == CSSValueID::Auto) {
isAuto = true;
}
}
} else {
isAuto = value->id() == CSSValueID::Auto;
}

double ratio = 0.0;
if(pair) {
auto width = to<CSSNumberValue>(*pair->first()).value();
auto height = to<CSSNumberValue>(*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);
Expand Down
7 changes: 7 additions & 0 deletions source/layout/boxstyle.h
Original file line number Diff line number Diff line change
Expand Up @@ -453,6 +453,11 @@ inline float Length::calcMin(float maximum) const

using LengthList = std::vector<Length>;

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) {}
Expand Down Expand Up @@ -789,6 +794,8 @@ class BoxStyle : public HeapMember, public RefCounted<BoxStyle> {
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; }
Expand Down
8 changes: 8 additions & 0 deletions source/layout/replacedbox.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,14 @@ ReplacedBox::ReplacedBox(Node* node, const RefPtr<BoxStyle>& 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);
Expand Down