LibWeb/CSS: Implement CSSPerspective

Equivalent to the perspective() transform function.

+34 WPT subtests, and the transformvalue-normalization test now runs to
completion instead of throwing an error - though its cases still fail
until CSSTransformValue is implemented.
This commit is contained in:
Sam Atkins 2025-09-16 11:05:33 +01:00
parent 68ceacb0c5
commit 2ffbb284f2
12 changed files with 298 additions and 45 deletions

View File

@ -133,6 +133,7 @@ set(SOURCES
CSS/CSSNumericValue.cpp
CSS/CSSPageRule.cpp
CSS/CSSPageDescriptors.cpp
CSS/CSSPerspective.cpp
CSS/CSSPropertyRule.cpp
CSS/CSSRotate.cpp
CSS/CSSRule.cpp

View File

@ -0,0 +1,160 @@
/*
* Copyright (c) 2025, Sam Atkins <sam@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include "CSSPerspective.h"
#include <LibWeb/Bindings/CSSPerspectivePrototype.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(CSSPerspective);
static WebIDL::ExceptionOr<CSSPerspectiveValueInternal> to_internal(JS::Realm& realm, CSSPerspectiveValue const& value)
{
// Steps 1 and 2 of The CSSPerspective(length) constructor:
// https://drafts.css-houdini.org/css-typed-om-1/#dom-cssperspective-cssperspective
return value.visit(
// 1. If length is a CSSNumericValue:
[](GC::Root<CSSNumericValue> const& numeric_value) -> WebIDL::ExceptionOr<CSSPerspectiveValueInternal> {
// 1. If length does not match <length>, throw a TypeError.
if (!numeric_value->type().matches_length({})) {
return WebIDL::SimpleException { WebIDL::SimpleExceptionType::TypeError, "CSSPerspective length component doesn't match <length>"sv };
}
return { GC::Ref { *numeric_value } };
},
// 2. Otherwise (that is, if length is not a CSSNumericValue):
[&realm](CSSKeywordish const& keywordish) -> WebIDL::ExceptionOr<CSSPerspectiveValueInternal> {
// 1. Rectify a keywordish value from length, then set length to the results value.
auto rectified_length = rectify_a_keywordish_value(realm, keywordish);
// 2. If length does not represent a value that is an ASCII case-insensitive match for the keyword none,
// throw a TypeError.
if (!rectified_length->value().equals_ignoring_ascii_case("none"_fly_string)) {
return WebIDL::SimpleException { WebIDL::SimpleExceptionType::TypeError, "CSSPerspective length component is a keyword other than `none`"sv };
}
return { rectified_length };
});
}
GC::Ref<CSSPerspective> CSSPerspective::create(JS::Realm& realm, CSSPerspectiveValueInternal length)
{
return realm.create<CSSPerspective>(realm, length);
}
// https://drafts.css-houdini.org/css-typed-om-1/#dom-cssperspective-cssperspective
WebIDL::ExceptionOr<GC::Ref<CSSPerspective>> CSSPerspective::construct_impl(JS::Realm& realm, CSSPerspectiveValue length)
{
// The CSSPerspective(length) constructor must, when invoked, perform the following steps:
// NB: Steps 1 and 2 are implemented in to_internal().
auto internal_length = TRY(to_internal(realm, length));
// 3. Return a new CSSPerspective object with its length internal slot set to length, and its is2D internal slot
// set to false.
return CSSPerspective::create(realm, internal_length);
}
CSSPerspective::CSSPerspective(JS::Realm& realm, CSSPerspectiveValueInternal length)
: CSSTransformComponent(realm, Is2D::No)
, m_length(length)
{
}
CSSPerspective::~CSSPerspective() = default;
void CSSPerspective::initialize(JS::Realm& realm)
{
WEB_SET_PROTOTYPE_FOR_INTERFACE(CSSPerspective);
Base::initialize(realm);
}
void CSSPerspective::visit_edges(Visitor& visitor)
{
Base::visit_edges(visitor);
m_length.visit([&visitor](auto const& it) { visitor.visit(it); });
}
// https://drafts.css-houdini.org/css-typed-om-1/#serialize-a-cssperspective
WebIDL::ExceptionOr<Utf16String> CSSPerspective::to_string() const
{
// 1. Let s initially be "perspective(".
StringBuilder builder { StringBuilder::Mode::UTF16 };
builder.append("perspective("sv);
// 2. Serialize thiss length internal slot, with a minimum of 0px, and append it to s.
auto serialized_length = m_length.visit(
[](GC::Ref<CSSNumericValue> const& numeric_value) {
return numeric_value->to_string({ .minimum = 0 });
},
[](GC::Ref<CSSKeywordValue> const& keyword_value) {
return keyword_value->to_string();
});
builder.append(serialized_length);
// 3. Append ")" to s, and return s.
builder.append(")"sv);
return builder.to_utf16_string();
}
WebIDL::ExceptionOr<GC::Ref<Geometry::DOMMatrix>> CSSPerspective::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.
auto matrix = Geometry::DOMMatrix::create(realm());
TRY(m_length.visit(
[&matrix](GC::Ref<CSSNumericValue> const& numeric_value) -> WebIDL::ExceptionOr<void> {
// NB: to() throws a TypeError if the conversion can't be done.
auto distance = TRY(numeric_value->to("px"_fly_string))->value();
matrix->set_m34(-1 / (distance <= 0 ? 1 : distance));
return {};
},
[](GC::Ref<CSSKeywordValue> const&) -> WebIDL::ExceptionOr<void> {
// NB: This is `none`, so do nothing.
return {};
}));
// 2. Return matrix.
return matrix;
}
CSSPerspectiveValue CSSPerspective::length() const
{
return m_length.visit(
[](GC::Ref<CSSNumericValue> const& numeric_value) -> CSSPerspectiveValue {
return GC::Root { numeric_value };
},
[](GC::Ref<CSSKeywordValue> const& keyword_value) -> CSSPerspectiveValue {
return CSSKeywordish { keyword_value };
});
}
WebIDL::ExceptionOr<void> CSSPerspective::set_length(CSSPerspectiveValue value)
{
// AD-HOC: Not specced. https://github.com/w3c/css-houdini-drafts/issues/1153
// WPT expects this to throw for invalid values, so just reuse the constructor code.
auto length = TRY(to_internal(realm(), value));
m_length = length;
return {};
}
// https://drafts.css-houdini.org/css-typed-om-1/#dom-cssperspective-is2d
void CSSPerspective::set_is_2d(bool)
{
// The is2D attribute of a CSSPerspective object must, on setting, do nothing.
}
}

View File

@ -0,0 +1,52 @@
/*
* Copyright (c) 2025, Sam Atkins <sam@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <LibWeb/CSS/CSSKeywordValue.h>
#include <LibWeb/CSS/CSSNumericValue.h>
#include <LibWeb/CSS/CSSTransformComponent.h>
namespace Web::CSS {
// https://drafts.css-houdini.org/css-typed-om-1/#typedefdef-cssperspectivevalue
// NB: CSSKeywordish is flattened here, because our bindings generator flattens nested variants.
using CSSPerspectiveValue = Variant<GC::Root<CSSNumericValue>, String, GC::Root<CSSKeywordValue>>;
using CSSPerspectiveValueInternal = Variant<GC::Ref<CSSNumericValue>, GC::Ref<CSSKeywordValue>>;
// https://drafts.css-houdini.org/css-typed-om-1/#cssperspective
class CSSPerspective final : public CSSTransformComponent {
WEB_PLATFORM_OBJECT(CSSPerspective, CSSTransformComponent);
GC_DECLARE_ALLOCATOR(CSSPerspective);
public:
[[nodiscard]] static GC::Ref<CSSPerspective> create(JS::Realm&, CSSPerspectiveValueInternal);
static WebIDL::ExceptionOr<GC::Ref<CSSPerspective>> construct_impl(JS::Realm&, CSSPerspectiveValue);
virtual ~CSSPerspective() override;
virtual WebIDL::ExceptionOr<Utf16String> to_string() const override;
virtual WebIDL::ExceptionOr<GC::Ref<Geometry::DOMMatrix>> to_matrix() const override;
CSSPerspectiveValue length() const;
WebIDL::ExceptionOr<void> set_length(CSSPerspectiveValue value);
virtual void set_is_2d(bool value) override;
private:
explicit CSSPerspective(JS::Realm&, CSSPerspectiveValueInternal);
virtual void initialize(JS::Realm&) override;
virtual void visit_edges(Visitor&) override;
CSSPerspectiveValueInternal m_length;
};
}
class CSSPerspective {
};

View File

@ -0,0 +1,12 @@
#import <CSS/CSSKeywordValue.idl>
#import <CSS/CSSNumericValue.idl>
#import <CSS/CSSTransformComponent.idl>
typedef (CSSNumericValue or CSSKeywordish) CSSPerspectiveValue;
// https://drafts.css-houdini.org/css-typed-om-1/#cssperspective
[Exposed=(Window, Worker, PaintWorklet, LayoutWorklet)]
interface CSSPerspective : CSSTransformComponent {
constructor(CSSPerspectiveValue length);
attribute CSSPerspectiveValue length;
};

View File

@ -258,6 +258,7 @@ class CSSNumericArray;
class CSSNumericValue;
class CSSPageRule;
class CSSPageDescriptors;
class CSSPerspective;
class CSSPropertyRule;
class CSSRotate;
class CSSRule;

View File

@ -53,6 +53,7 @@ libweb_js_bindings(CSS/CSSNumericArray)
libweb_js_bindings(CSS/CSSNumericValue)
libweb_js_bindings(CSS/CSSPageRule)
libweb_js_bindings(CSS/CSSPageDescriptors)
libweb_js_bindings(CSS/CSSPerspective)
libweb_js_bindings(CSS/CSSPropertyRule)
libweb_js_bindings(CSS/CSSRotate)
libweb_js_bindings(CSS/CSSRule)

View File

@ -65,6 +65,7 @@ CSSNumericArray
CSSNumericValue
CSSPageDescriptors
CSSPageRule
CSSPerspective
CSSPropertyRule
CSSRotate
CSSRule

View File

@ -2,8 +2,8 @@ Harness status: OK
Found 545 tests
346 Pass
199 Fail
353 Pass
192 Fail
Pass idl_test setup
Pass idl_test validation
Pass Partial interface Element: original interface defined
@ -364,13 +364,13 @@ Fail Stringification of skewY
Fail CSSSkewY interface: skewY must inherit property "ay" with the proper type
Fail CSSTransformComponent interface: skewY must inherit property "is2D" with the proper type
Fail CSSTransformComponent interface: skewY must inherit property "toMatrix()" with the proper type
Fail CSSPerspective interface: existence and properties of interface object
Fail CSSPerspective interface object length
Fail CSSPerspective interface object name
Fail CSSPerspective interface: existence and properties of interface prototype object
Fail CSSPerspective interface: existence and properties of interface prototype object's "constructor" property
Fail CSSPerspective interface: existence and properties of interface prototype object's @@unscopables property
Fail CSSPerspective interface: attribute length
Pass CSSPerspective interface: existence and properties of interface object
Pass CSSPerspective interface object length
Pass CSSPerspective interface object name
Pass CSSPerspective interface: existence and properties of interface prototype object
Pass CSSPerspective interface: existence and properties of interface prototype object's "constructor" property
Pass CSSPerspective interface: existence and properties of interface prototype object's @@unscopables property
Pass CSSPerspective interface: attribute length
Fail CSSPerspective must be primary interface of perspective
Fail Stringification of perspective
Fail CSSPerspective interface: perspective must inherit property "length" with the proper type

View File

@ -1,7 +1,33 @@
Harness status: Error
Harness status: OK
Found 2 tests
Found 28 tests
2 Fail
28 Fail
Fail Normalizing a matrix() returns a CSSMatrixComponent
Fail Normalizing a matrix3d() returns a CSSMatrixComponent
Fail Normalizing a matrix3d() returns a CSSMatrixComponent
Fail Normalizing a translate() with X returns a CSSTranslate
Fail Normalizing a translate() with X and Y returns a CSSTranslate
Fail Normalizing a translateX() returns a CSSTranslate
Fail Normalizing a translateY() returns a CSSTranslate
Fail Normalizing a translate3d() returns a CSSTranslate
Fail Normalizing a translateZ() returns a CSSTranslate
Fail Normalizing a scale() with one argument returns a CSSScale
Fail Normalizing a scale() with two arguments returns a CSSScale
Fail Normalizing a scaleX() returns a CSSScale
Fail Normalizing a scaleY() returns a CSSScale
Fail Normalizing a scale3d() returns a CSSScale
Fail Normalizing a scaleZ() returns a CSSScale
Fail Normalizing a rotate() returns a CSSRotate
Fail Normalizing a rotate3d() returns a CSSRotate
Fail Normalizing a rotateX() returns a CSSRotate
Fail Normalizing a rotateY() returns a CSSRotate
Fail Normalizing a rotateZ() returns a CSSRotate
Fail Normalizing a skew() with only X returns a CSSSkew
Fail Normalizing a skew() with X and Y which is 0 value returns a CSSSkew
Fail Normalizing a skew() with X and Y returns a CSSSkew
Fail Normalizing a skewX() returns a CSSSkewX
Fail Normalizing a skewY() returns a CSSSkewY
Fail Normalizing a perspective() returns a CSSPerspective
Fail Normalizing a perspective(none) returns a CSSPerspective
Fail Normalizing a <transform-list> returns a CSSTransformValue containing all the transforms
Fail Normalizing transforms with calc values contains CSSMathValues

View File

@ -2,29 +2,29 @@ Harness status: OK
Found 25 tests
25 Fail
Fail Constructing a CSSPerspective with a keyword other than none (string) throws a TypeError
Fail Constructing a CSSPerspective with a keyword other than none (CSSKeywordValue) throws a TypeError
Fail Constructing a CSSPerspective with a double throws a TypeError
Fail Constructing a CSSPerspective with a unitless zero throws a TypeError
Fail Constructing a CSSPerspective with a string length throws a TypeError
Fail Constructing a CSSPerspective with a number CSSUnitValue throws a TypeError
Fail Constructing a CSSPerspective with a time dimension CSSUnitValue throws a TypeError
Fail Constructing a CSSPerspective with a CSSMathValue of angle type throws a TypeError
Fail Updating CSSPerspective.length with a keyword other than none (string) throws a TypeError
Fail Updating CSSPerspective.length with a keyword other than none (CSSKeywordValue) throws a TypeError
Fail Updating CSSPerspective.length with a double throws a TypeError
Fail Updating CSSPerspective.length with a unitless zero throws a TypeError
Fail Updating CSSPerspective.length with a string length throws a TypeError
Fail Updating CSSPerspective.length with a number CSSUnitValue throws a TypeError
Fail Updating CSSPerspective.length with a time dimension CSSUnitValue throws a TypeError
Fail Updating CSSPerspective.length with a CSSMathValue of angle type throws a TypeError
Fail CSSPerspective can be constructed from a length CSSUnitValue
Fail CSSPerspective.length can be updated to a length CSSUnitValue
Fail CSSPerspective can be constructed from a CSSMathValue of length type
Fail CSSPerspective.length can be updated to a CSSMathValue of length type
Fail CSSPerspective can be constructed from none (CSSKeywordValue)
Fail CSSPerspective.length can be updated to none (CSSKeywordValue)
Fail CSSPerspective can be constructed from none (string)
Fail CSSPerspective.length can be updated to none (string)
Fail Modifying CSSPerspective.is2D is a no-op
25 Pass
Pass Constructing a CSSPerspective with a keyword other than none (string) throws a TypeError
Pass Constructing a CSSPerspective with a keyword other than none (CSSKeywordValue) throws a TypeError
Pass Constructing a CSSPerspective with a double throws a TypeError
Pass Constructing a CSSPerspective with a unitless zero throws a TypeError
Pass Constructing a CSSPerspective with a string length throws a TypeError
Pass Constructing a CSSPerspective with a number CSSUnitValue throws a TypeError
Pass Constructing a CSSPerspective with a time dimension CSSUnitValue throws a TypeError
Pass Constructing a CSSPerspective with a CSSMathValue of angle type throws a TypeError
Pass Updating CSSPerspective.length with a keyword other than none (string) throws a TypeError
Pass Updating CSSPerspective.length with a keyword other than none (CSSKeywordValue) throws a TypeError
Pass Updating CSSPerspective.length with a double throws a TypeError
Pass Updating CSSPerspective.length with a unitless zero throws a TypeError
Pass Updating CSSPerspective.length with a string length throws a TypeError
Pass Updating CSSPerspective.length with a number CSSUnitValue throws a TypeError
Pass Updating CSSPerspective.length with a time dimension CSSUnitValue throws a TypeError
Pass Updating CSSPerspective.length with a CSSMathValue of angle type throws a TypeError
Pass CSSPerspective can be constructed from a length CSSUnitValue
Pass CSSPerspective.length can be updated to a length CSSUnitValue
Pass CSSPerspective can be constructed from a CSSMathValue of length type
Pass CSSPerspective.length can be updated to a CSSMathValue of length type
Pass CSSPerspective can be constructed from none (CSSKeywordValue)
Pass CSSPerspective.length can be updated to none (CSSKeywordValue)
Pass CSSPerspective can be constructed from none (string)
Pass CSSPerspective.length can be updated to none (string)
Pass Modifying CSSPerspective.is2D is a no-op

View File

@ -2,7 +2,6 @@ Harness status: OK
Found 2 tests
1 Pass
1 Fail
2 Pass
Pass CSSTranslate.toMatrix() containing relative units throws TypeError
Fail CSSPerspective.toMatrix() containing relative units throws TypeError
Pass CSSPerspective.toMatrix() containing relative units throws TypeError

View File

@ -2,13 +2,13 @@ Harness status: OK
Found 8 tests
6 Pass
2 Fail
7 Pass
1 Fail
Pass CSSTranslate.toMatrix() returns correct matrix
Pass CSSRotate.toMatrix() returns correct matrix
Pass CSSScale.toMatrix() returns correct matrix
Pass CSSSkew.toMatrix() returns correct matrix
Pass CSSSkewX.toMatrix() returns correct matrix
Pass CSSSkewY.toMatrix() returns correct matrix
Fail CSSPerspective.toMatrix() returns correct matrix
Pass CSSPerspective.toMatrix() returns correct matrix
Fail CSSMatrixComponent.toMatrix() returns correct matrix