LibWeb/CSS: Implement CSSScale

Equivalent to the scale() transform functions.

+33 WPT subtests.
This commit is contained in:
Sam Atkins 2025-09-15 14:31:03 +01:00
parent d348d8d9b8
commit 456946368e
11 changed files with 304 additions and 40 deletions

View File

@ -137,6 +137,7 @@ set(SOURCES
CSS/CSSRotate.cpp
CSS/CSSRule.cpp
CSS/CSSRuleList.cpp
CSS/CSSScale.cpp
CSS/CSSStyleDeclaration.cpp
CSS/CSSStyleProperties.cpp
CSS/CSSStyleRule.cpp

View File

@ -0,0 +1,202 @@
/*
* Copyright (c) 2025, Sam Atkins <sam@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include "CSSScale.h"
#include <LibWeb/Bindings/CSSScalePrototype.h>
#include <LibWeb/Bindings/Intrinsics.h>
#include <LibWeb/CSS/CSSNumericValue.h>
#include <LibWeb/CSS/CSSUnitValue.h>
#include <LibWeb/Geometry/DOMMatrix.h>
#include <LibWeb/WebIDL/ExceptionOr.h>
namespace Web::CSS {
GC_DEFINE_ALLOCATOR(CSSScale);
GC::Ref<CSSScale> CSSScale::create(JS::Realm& realm, Is2D is_2d, GC::Ref<CSSNumericValue> x, GC::Ref<CSSNumericValue> y, GC::Ref<CSSNumericValue> z)
{
return realm.create<CSSScale>(realm, is_2d, x, y, z);
}
WebIDL::ExceptionOr<GC::Ref<CSSScale>> CSSScale::construct_impl(JS::Realm& realm, CSSNumberish x, CSSNumberish y, Optional<CSSNumberish> z)
{
// The CSSScale(x, y, z) constructor must, when invoked, perform the following steps:
// 1. Let x, y, and z (if passed) be replaced by the result of rectifying a numberish value.
auto rectified_x = rectify_a_numberish_value(realm, x);
auto rectified_y = rectify_a_numberish_value(realm, y);
auto rectified_z = z.map([&](auto& it) { return rectify_a_numberish_value(realm, it); });
// 2. If x, y, or z (if passed) dont match <number>, throw a TypeError.
if (!rectified_x->type().matches_number({}))
return WebIDL::SimpleException { WebIDL::SimpleExceptionType::TypeError, "CSSScale x component doesn't match <number>"sv };
if (!rectified_y->type().matches_number({}))
return WebIDL::SimpleException { WebIDL::SimpleExceptionType::TypeError, "CSSScale y component doesn't match <number>"sv };
if (rectified_z.has_value() && !rectified_z.value()->type().matches_number({}))
return WebIDL::SimpleException { WebIDL::SimpleExceptionType::TypeError, "CSSScale z component doesn't match <number>"sv };
// 3. Let this be a new CSSScale object, with its x and y internal slots set to x and y.
// 4. If z was passed, set thiss z internal slot to z, and set thiss is2D internal slot to false.
// 5. If z was not passed, set thiss z internal slot to a new unit value of (1, "number"), and set thiss is2D internal slot to true.
Is2D is_2d = Is2D::No;
if (!rectified_z.has_value()) {
rectified_z = CSSUnitValue::create(realm, 1, "number"_fly_string);
is_2d = Is2D::Yes;
}
auto this_ = CSSScale::create(realm, is_2d, rectified_x, rectified_y, rectified_z.release_value());
// 6. Return this.
return this_;
}
CSSScale::CSSScale(JS::Realm& realm, Is2D is_2d, GC::Ref<CSSNumericValue> x, GC::Ref<CSSNumericValue> y, GC::Ref<CSSNumericValue> z)
: CSSTransformComponent(realm, is_2d)
, m_x(x)
, m_y(y)
, m_z(z)
{
}
CSSScale::~CSSScale() = default;
void CSSScale::initialize(JS::Realm& realm)
{
WEB_SET_PROTOTYPE_FOR_INTERFACE(CSSScale);
Base::initialize(realm);
}
void CSSScale::visit_edges(Visitor& visitor)
{
Base::visit_edges(visitor);
visitor.visit(m_x);
visitor.visit(m_y);
visitor.visit(m_z);
}
// https://drafts.css-houdini.org/css-typed-om-1/#serialize-a-cssscale
WebIDL::ExceptionOr<Utf16String> CSSScale::to_string() const
{
// 1. Let s initially be the empty string.
StringBuilder builder { StringBuilder::Mode::UTF16 };
// 2. If thiss is2D internal slot is false:
if (!is_2d()) {
// 1. Append "scale3d(" to s.
builder.append("scale3d("sv);
// 2. Serialize thiss x internal slot, and append it to s.
builder.append(m_x->to_string());
// 3. Append ", " to s.
builder.append(", "sv);
// 4. Serialize thiss y internal slot, and append it to s.
builder.append(m_y->to_string());
// 5. Append ", " to s.
builder.append(", "sv);
// 6. Serialize thiss z internal slot, and append it to s.
builder.append(m_z->to_string());
// 7. Append ")" to s, and return s.
builder.append(")"sv);
return builder.to_utf16_string();
}
// 3. Otherwise:
else {
// 1. Append "scale(" to s.
builder.append("scale("sv);
// 2. Serialize thiss x internal slot, and append it to s.
builder.append(m_x->to_string());
// 3. If thiss x and y internal slots are equal numeric values, append ")" to s and return s.
if (m_x->is_equal_numeric_value(m_y)) {
builder.append(")"sv);
return builder.to_utf16_string();
}
// 4. Otherwise, append ", " to s.
builder.append(", "sv);
// 5. Serialize thiss y internal slot, and append it to s.
builder.append(m_y->to_string());
// 6. Append ")" to s, and return s.
builder.append(")"sv);
return builder.to_utf16_string();
}
}
// https://drafts.css-houdini.org/css-typed-om-1/#dom-csstransformcomponent-tomatrix
WebIDL::ExceptionOr<GC::Ref<Geometry::DOMMatrix>> CSSScale::to_matrix() const
{
// 1. Let matrix be a new DOMMatrix object, initialized to thiss equivalent 4x4 transform matrix, as defined in
// CSS Transforms 1 § 12. Mathematical Description of Transform Functions, and with its is2D internal slot set
// to the same value as thiss is2D internal slot.
// NOTE: Recall that the is2D flag affects what transform, and thus what equivalent matrix, a
// CSSTransformComponent represents.
// As the entries of such a matrix are defined relative to the px unit, if any <length>s in this involved in
// generating the matrix are not compatible units with px (such as relative lengths or percentages), throw a
// TypeError.
// 2. Return matrix.
auto matrix = Geometry::DOMMatrix::create(realm());
// NB: to() throws a TypeError if the conversion can't be done.
auto x = TRY(m_x->to("number"_fly_string))->value();
auto y = TRY(m_y->to("number"_fly_string))->value();
if (is_2d())
return matrix->scale_self(x, y, {}, {}, {}, {});
auto z = TRY(m_z->to("number"_fly_string))->value();
return matrix->scale_self(x, y, z, {}, {}, {});
}
// https://drafts.css-houdini.org/css-typed-om-1/#dom-cssscale-x
WebIDL::ExceptionOr<void> CSSScale::set_x(CSSNumberish value)
{
// The x, y, and z attributes must, on setting to a new value val, rectify a numberish value from val and set the
// corresponding internal slot to the result of that.
// AD-HOC: WPT expects this to throw for invalid values. https://github.com/w3c/css-houdini-drafts/issues/1153
auto rectified_x = rectify_a_numberish_value(realm(), value);
if (!rectified_x->type().matches_number({}))
return WebIDL::SimpleException { WebIDL::SimpleExceptionType::TypeError, "CSSScale x component doesn't match <number>"sv };
m_x = rectified_x;
return {};
}
// https://drafts.css-houdini.org/css-typed-om-1/#dom-cssscale-y
WebIDL::ExceptionOr<void> CSSScale::set_y(CSSNumberish value)
{
// The x, y, and z attributes must, on setting to a new value val, rectify a numberish value from val and set the
// corresponding internal slot to the result of that.
// AD-HOC: WPT expects this to throw for invalid values. https://github.com/w3c/css-houdini-drafts/issues/1153
auto rectified_y = rectify_a_numberish_value(realm(), value);
if (!rectified_y->type().matches_number({}))
return WebIDL::SimpleException { WebIDL::SimpleExceptionType::TypeError, "CSSScale y component doesn't match <number>"sv };
m_y = rectified_y;
return {};
}
// https://drafts.css-houdini.org/css-typed-om-1/#dom-cssscale-z
WebIDL::ExceptionOr<void> CSSScale::set_z(CSSNumberish value)
{
// The x, y, and z attributes must, on setting to a new value val, rectify a numberish value from val and set the
// corresponding internal slot to the result of that.
// AD-HOC: WPT expects this to throw for invalid values. https://github.com/w3c/css-houdini-drafts/issues/1153
auto rectified_z = rectify_a_numberish_value(realm(), value);
if (!rectified_z->type().matches_number({}))
return WebIDL::SimpleException { WebIDL::SimpleExceptionType::TypeError, "CSSScale z component doesn't match <number>"sv };
m_z = rectified_z;
return {};
}
}

View File

@ -0,0 +1,47 @@
/*
* Copyright (c) 2025, Sam Atkins <sam@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <LibWeb/CSS/CSSNumericValue.h>
#include <LibWeb/CSS/CSSTransformComponent.h>
namespace Web::CSS {
// https://drafts.css-houdini.org/css-typed-om-1/#cssscale
class CSSScale final : public CSSTransformComponent {
WEB_PLATFORM_OBJECT(CSSScale, CSSTransformComponent);
GC_DECLARE_ALLOCATOR(CSSScale);
public:
[[nodiscard]] static GC::Ref<CSSScale> create(JS::Realm&, Is2D, GC::Ref<CSSNumericValue> x, GC::Ref<CSSNumericValue> y, GC::Ref<CSSNumericValue> z);
static WebIDL::ExceptionOr<GC::Ref<CSSScale>> construct_impl(JS::Realm&, CSSNumberish x, CSSNumberish y, Optional<CSSNumberish> z = {});
virtual ~CSSScale() override;
virtual WebIDL::ExceptionOr<Utf16String> to_string() const override;
virtual WebIDL::ExceptionOr<GC::Ref<Geometry::DOMMatrix>> to_matrix() const override;
CSSNumberish x() const { return GC::Root { m_x }; }
CSSNumberish y() const { return GC::Root { m_y }; }
CSSNumberish z() const { return GC::Root { m_z }; }
WebIDL::ExceptionOr<void> set_x(CSSNumberish value);
WebIDL::ExceptionOr<void> set_y(CSSNumberish value);
WebIDL::ExceptionOr<void> set_z(CSSNumberish value);
private:
explicit CSSScale(JS::Realm&, Is2D, GC::Ref<CSSNumericValue> x, GC::Ref<CSSNumericValue> y, GC::Ref<CSSNumericValue> z);
virtual void initialize(JS::Realm&) override;
virtual void visit_edges(Visitor&) override;
GC::Ref<CSSNumericValue> m_x;
GC::Ref<CSSNumericValue> m_y;
GC::Ref<CSSNumericValue> m_z;
};
}

View File

@ -0,0 +1,11 @@
#import <CSS/CSSNumericValue.idl>
#import <CSS/CSSTransformComponent.idl>
// https://drafts.css-houdini.org/css-typed-om-1/#cssscale
[Exposed=(Window, Worker, PaintWorklet, LayoutWorklet)]
interface CSSScale : CSSTransformComponent {
constructor(CSSNumberish x, CSSNumberish y, optional CSSNumberish z);
attribute CSSNumberish x;
attribute CSSNumberish y;
attribute CSSNumberish z;
};

View File

@ -262,6 +262,7 @@ class CSSPropertyRule;
class CSSRotate;
class CSSRule;
class CSSRuleList;
class CSSScale;
class CSSStyleDeclaration;
class CSSStyleProperties;
class CSSStyleRule;

View File

@ -57,6 +57,7 @@ libweb_js_bindings(CSS/CSSPropertyRule)
libweb_js_bindings(CSS/CSSRotate)
libweb_js_bindings(CSS/CSSRule)
libweb_js_bindings(CSS/CSSRuleList)
libweb_js_bindings(CSS/CSSScale)
libweb_js_bindings(CSS/CSSStyleDeclaration)
libweb_js_bindings(CSS/CSSStyleProperties)
libweb_js_bindings(CSS/CSSStyleRule)

View File

@ -69,6 +69,7 @@ CSSPropertyRule
CSSRotate
CSSRule
CSSRuleList
CSSScale
CSSStyleDeclaration
CSSStyleProperties
CSSStyleRule

View File

@ -2,8 +2,8 @@ Harness status: OK
Found 545 tests
315 Pass
230 Fail
324 Pass
221 Fail
Pass idl_test setup
Pass idl_test validation
Pass Partial interface Element: original interface defined
@ -310,15 +310,15 @@ Fail CSSRotate interface: rotate must inherit property "z" with the proper type
Fail CSSRotate interface: rotate must inherit property "angle" with the proper type
Fail CSSTransformComponent interface: rotate must inherit property "is2D" with the proper type
Fail CSSTransformComponent interface: rotate must inherit property "toMatrix()" with the proper type
Fail CSSScale interface: existence and properties of interface object
Fail CSSScale interface object length
Fail CSSScale interface object name
Fail CSSScale interface: existence and properties of interface prototype object
Fail CSSScale interface: existence and properties of interface prototype object's "constructor" property
Fail CSSScale interface: existence and properties of interface prototype object's @@unscopables property
Fail CSSScale interface: attribute x
Fail CSSScale interface: attribute y
Fail CSSScale interface: attribute z
Pass CSSScale interface: existence and properties of interface object
Pass CSSScale interface object length
Pass CSSScale interface object name
Pass CSSScale interface: existence and properties of interface prototype object
Pass CSSScale interface: existence and properties of interface prototype object's "constructor" property
Pass CSSScale interface: existence and properties of interface prototype object's @@unscopables property
Pass CSSScale interface: attribute x
Pass CSSScale interface: attribute y
Pass CSSScale interface: attribute z
Fail CSSScale must be primary interface of scale
Fail Stringification of scale
Fail CSSScale interface: scale must inherit property "x" with the proper type

View File

@ -2,26 +2,26 @@ Harness status: OK
Found 22 tests
22 Fail
Fail Constructing a CSSScale with an angle CSSUnitValue for the coordinates throws a TypeError
Fail Constructing a CSSScale with a CSSMathValue that doesn't match <number> for the coordinates throws a TypeError
Fail Updating CSSScale.x to an angle CSSUnitValue throws a TypeError
Fail Updating CSSScale.x to a CSSMathValue that doesn't match <number> throws a TypeError
Fail Updating CSSScale.y to an angle CSSUnitValue throws a TypeError
Fail Updating CSSScale.y to a CSSMathValue that doesn't match <number> throws a TypeError
Fail Updating CSSScale.z to an angle CSSUnitValue throws a TypeError
Fail Updating CSSScale.z to a CSSMathValue that doesn't match <number> throws a TypeError
Fail CSSScale can be constructed from two number coordinates
Fail CSSScale can be constructed from three number coordinates
Fail CSSScale can be constructed from CSSMathValue coordinates
Fail CSSScale can be constructed from unit canceling length value coordinates
Fail CSSScale.x can be updated to a number
Fail CSSScale.x can be updated to a numberish
Fail CSSScale.x can be updated to a CSSMathValue
Fail CSSScale.y can be updated to a number
Fail CSSScale.y can be updated to a numberish
Fail CSSScale.y can be updated to a CSSMathValue
Fail CSSScale.z can be updated to a number
Fail CSSScale.z can be updated to a numberish
Fail CSSScale.z can be updated to a CSSMathValue
Fail Modifying CSSScale.is2D can be updated to true or false
22 Pass
Pass Constructing a CSSScale with an angle CSSUnitValue for the coordinates throws a TypeError
Pass Constructing a CSSScale with a CSSMathValue that doesn't match <number> for the coordinates throws a TypeError
Pass Updating CSSScale.x to an angle CSSUnitValue throws a TypeError
Pass Updating CSSScale.x to a CSSMathValue that doesn't match <number> throws a TypeError
Pass Updating CSSScale.y to an angle CSSUnitValue throws a TypeError
Pass Updating CSSScale.y to a CSSMathValue that doesn't match <number> throws a TypeError
Pass Updating CSSScale.z to an angle CSSUnitValue throws a TypeError
Pass Updating CSSScale.z to a CSSMathValue that doesn't match <number> throws a TypeError
Pass CSSScale can be constructed from two number coordinates
Pass CSSScale can be constructed from three number coordinates
Pass CSSScale can be constructed from CSSMathValue coordinates
Pass CSSScale can be constructed from unit canceling length value coordinates
Pass CSSScale.x can be updated to a number
Pass CSSScale.x can be updated to a numberish
Pass CSSScale.x can be updated to a CSSMathValue
Pass CSSScale.y can be updated to a number
Pass CSSScale.y can be updated to a numberish
Pass CSSScale.y can be updated to a CSSMathValue
Pass CSSScale.z can be updated to a number
Pass CSSScale.z can be updated to a numberish
Pass CSSScale.z can be updated to a CSSMathValue
Pass Modifying CSSScale.is2D can be updated to true or false

View File

@ -2,9 +2,9 @@ Harness status: OK
Found 4 tests
2 Pass
2 Fail
3 Pass
1 Fail
Pass CSSTranslate.toMatrix() flattens when told it is 2d
Pass CSSRotate.toMatrix() flattens when told it is 2d
Fail CSSScale.toMatrix() flattens when told it is 2d
Pass CSSScale.toMatrix() flattens when told it is 2d
Fail CSSMatrixComponent.toMatrix() flattens when told it is 2d

View File

@ -2,11 +2,11 @@ Harness status: OK
Found 8 tests
2 Pass
6 Fail
3 Pass
5 Fail
Pass CSSTranslate.toMatrix() returns correct matrix
Pass CSSRotate.toMatrix() returns correct matrix
Fail CSSScale.toMatrix() returns correct matrix
Pass CSSScale.toMatrix() returns correct matrix
Fail CSSSkew.toMatrix() returns correct matrix
Fail CSSSkewX.toMatrix() returns correct matrix
Fail CSSSkewY.toMatrix() returns correct matrix