Skip to content

Commit 2ffbb28

Browse files
committed
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.
1 parent 68ceacb commit 2ffbb28

File tree

12 files changed

+298
-45
lines changed

12 files changed

+298
-45
lines changed

Libraries/LibWeb/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,7 @@ set(SOURCES
133133
CSS/CSSNumericValue.cpp
134134
CSS/CSSPageRule.cpp
135135
CSS/CSSPageDescriptors.cpp
136+
CSS/CSSPerspective.cpp
136137
CSS/CSSPropertyRule.cpp
137138
CSS/CSSRotate.cpp
138139
CSS/CSSRule.cpp
Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
/*
2+
* Copyright (c) 2025, Sam Atkins <sam@ladybird.org>
3+
*
4+
* SPDX-License-Identifier: BSD-2-Clause
5+
*/
6+
7+
#include "CSSPerspective.h"
8+
#include <LibWeb/Bindings/CSSPerspectivePrototype.h>
9+
#include <LibWeb/Bindings/Intrinsics.h>
10+
#include <LibWeb/CSS/CSSNumericValue.h>
11+
#include <LibWeb/CSS/CSSUnitValue.h>
12+
#include <LibWeb/Geometry/DOMMatrix.h>
13+
#include <LibWeb/WebIDL/ExceptionOr.h>
14+
15+
namespace Web::CSS {
16+
17+
GC_DEFINE_ALLOCATOR(CSSPerspective);
18+
19+
static WebIDL::ExceptionOr<CSSPerspectiveValueInternal> to_internal(JS::Realm& realm, CSSPerspectiveValue const& value)
20+
{
21+
// Steps 1 and 2 of The CSSPerspective(length) constructor:
22+
// https://drafts.css-houdini.org/css-typed-om-1/#dom-cssperspective-cssperspective
23+
return value.visit(
24+
// 1. If length is a CSSNumericValue:
25+
[](GC::Root<CSSNumericValue> const& numeric_value) -> WebIDL::ExceptionOr<CSSPerspectiveValueInternal> {
26+
// 1. If length does not match <length>, throw a TypeError.
27+
if (!numeric_value->type().matches_length({})) {
28+
return WebIDL::SimpleException { WebIDL::SimpleExceptionType::TypeError, "CSSPerspective length component doesn't match <length>"sv };
29+
}
30+
return { GC::Ref { *numeric_value } };
31+
},
32+
// 2. Otherwise (that is, if length is not a CSSNumericValue):
33+
[&realm](CSSKeywordish const& keywordish) -> WebIDL::ExceptionOr<CSSPerspectiveValueInternal> {
34+
// 1. Rectify a keywordish value from length, then set length to the result’s value.
35+
auto rectified_length = rectify_a_keywordish_value(realm, keywordish);
36+
37+
// 2. If length does not represent a value that is an ASCII case-insensitive match for the keyword none,
38+
// throw a TypeError.
39+
if (!rectified_length->value().equals_ignoring_ascii_case("none"_fly_string)) {
40+
return WebIDL::SimpleException { WebIDL::SimpleExceptionType::TypeError, "CSSPerspective length component is a keyword other than `none`"sv };
41+
}
42+
43+
return { rectified_length };
44+
});
45+
}
46+
47+
GC::Ref<CSSPerspective> CSSPerspective::create(JS::Realm& realm, CSSPerspectiveValueInternal length)
48+
{
49+
return realm.create<CSSPerspective>(realm, length);
50+
}
51+
52+
// https://drafts.css-houdini.org/css-typed-om-1/#dom-cssperspective-cssperspective
53+
WebIDL::ExceptionOr<GC::Ref<CSSPerspective>> CSSPerspective::construct_impl(JS::Realm& realm, CSSPerspectiveValue length)
54+
{
55+
// The CSSPerspective(length) constructor must, when invoked, perform the following steps:
56+
// NB: Steps 1 and 2 are implemented in to_internal().
57+
auto internal_length = TRY(to_internal(realm, length));
58+
59+
// 3. Return a new CSSPerspective object with its length internal slot set to length, and its is2D internal slot
60+
// set to false.
61+
return CSSPerspective::create(realm, internal_length);
62+
}
63+
64+
CSSPerspective::CSSPerspective(JS::Realm& realm, CSSPerspectiveValueInternal length)
65+
: CSSTransformComponent(realm, Is2D::No)
66+
, m_length(length)
67+
{
68+
}
69+
70+
CSSPerspective::~CSSPerspective() = default;
71+
72+
void CSSPerspective::initialize(JS::Realm& realm)
73+
{
74+
WEB_SET_PROTOTYPE_FOR_INTERFACE(CSSPerspective);
75+
Base::initialize(realm);
76+
}
77+
78+
void CSSPerspective::visit_edges(Visitor& visitor)
79+
{
80+
Base::visit_edges(visitor);
81+
m_length.visit([&visitor](auto const& it) { visitor.visit(it); });
82+
}
83+
84+
// https://drafts.css-houdini.org/css-typed-om-1/#serialize-a-cssperspective
85+
WebIDL::ExceptionOr<Utf16String> CSSPerspective::to_string() const
86+
{
87+
// 1. Let s initially be "perspective(".
88+
StringBuilder builder { StringBuilder::Mode::UTF16 };
89+
builder.append("perspective("sv);
90+
91+
// 2. Serialize this’s length internal slot, with a minimum of 0px, and append it to s.
92+
auto serialized_length = m_length.visit(
93+
[](GC::Ref<CSSNumericValue> const& numeric_value) {
94+
return numeric_value->to_string({ .minimum = 0 });
95+
},
96+
[](GC::Ref<CSSKeywordValue> const& keyword_value) {
97+
return keyword_value->to_string();
98+
});
99+
builder.append(serialized_length);
100+
101+
// 3. Append ")" to s, and return s.
102+
builder.append(")"sv);
103+
return builder.to_utf16_string();
104+
}
105+
106+
WebIDL::ExceptionOr<GC::Ref<Geometry::DOMMatrix>> CSSPerspective::to_matrix() const
107+
{
108+
// 1. Let matrix be a new DOMMatrix object, initialized to this’s equivalent 4x4 transform matrix, as defined in
109+
// CSS Transforms 1 § 12. Mathematical Description of Transform Functions, and with its is2D internal slot set
110+
// to the same value as this’s is2D internal slot.
111+
// NOTE: Recall that the is2D flag affects what transform, and thus what equivalent matrix, a
112+
// CSSTransformComponent represents.
113+
// As the entries of such a matrix are defined relative to the px unit, if any <length>s in this involved in
114+
// generating the matrix are not compatible units with px (such as relative lengths or percentages), throw a
115+
// TypeError.
116+
auto matrix = Geometry::DOMMatrix::create(realm());
117+
118+
TRY(m_length.visit(
119+
[&matrix](GC::Ref<CSSNumericValue> const& numeric_value) -> WebIDL::ExceptionOr<void> {
120+
// NB: to() throws a TypeError if the conversion can't be done.
121+
auto distance = TRY(numeric_value->to("px"_fly_string))->value();
122+
matrix->set_m34(-1 / (distance <= 0 ? 1 : distance));
123+
return {};
124+
},
125+
[](GC::Ref<CSSKeywordValue> const&) -> WebIDL::ExceptionOr<void> {
126+
// NB: This is `none`, so do nothing.
127+
return {};
128+
}));
129+
130+
// 2. Return matrix.
131+
return matrix;
132+
}
133+
134+
CSSPerspectiveValue CSSPerspective::length() const
135+
{
136+
return m_length.visit(
137+
[](GC::Ref<CSSNumericValue> const& numeric_value) -> CSSPerspectiveValue {
138+
return GC::Root { numeric_value };
139+
},
140+
[](GC::Ref<CSSKeywordValue> const& keyword_value) -> CSSPerspectiveValue {
141+
return CSSKeywordish { keyword_value };
142+
});
143+
}
144+
145+
WebIDL::ExceptionOr<void> CSSPerspective::set_length(CSSPerspectiveValue value)
146+
{
147+
// AD-HOC: Not specced. https://github.com/w3c/css-houdini-drafts/issues/1153
148+
// WPT expects this to throw for invalid values, so just reuse the constructor code.
149+
auto length = TRY(to_internal(realm(), value));
150+
m_length = length;
151+
return {};
152+
}
153+
154+
// https://drafts.css-houdini.org/css-typed-om-1/#dom-cssperspective-is2d
155+
void CSSPerspective::set_is_2d(bool)
156+
{
157+
// The is2D attribute of a CSSPerspective object must, on setting, do nothing.
158+
}
159+
160+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/*
2+
* Copyright (c) 2025, Sam Atkins <sam@ladybird.org>
3+
*
4+
* SPDX-License-Identifier: BSD-2-Clause
5+
*/
6+
7+
#pragma once
8+
9+
#include <LibWeb/CSS/CSSKeywordValue.h>
10+
#include <LibWeb/CSS/CSSNumericValue.h>
11+
#include <LibWeb/CSS/CSSTransformComponent.h>
12+
13+
namespace Web::CSS {
14+
15+
// https://drafts.css-houdini.org/css-typed-om-1/#typedefdef-cssperspectivevalue
16+
// NB: CSSKeywordish is flattened here, because our bindings generator flattens nested variants.
17+
using CSSPerspectiveValue = Variant<GC::Root<CSSNumericValue>, String, GC::Root<CSSKeywordValue>>;
18+
using CSSPerspectiveValueInternal = Variant<GC::Ref<CSSNumericValue>, GC::Ref<CSSKeywordValue>>;
19+
20+
// https://drafts.css-houdini.org/css-typed-om-1/#cssperspective
21+
class CSSPerspective final : public CSSTransformComponent {
22+
WEB_PLATFORM_OBJECT(CSSPerspective, CSSTransformComponent);
23+
GC_DECLARE_ALLOCATOR(CSSPerspective);
24+
25+
public:
26+
[[nodiscard]] static GC::Ref<CSSPerspective> create(JS::Realm&, CSSPerspectiveValueInternal);
27+
static WebIDL::ExceptionOr<GC::Ref<CSSPerspective>> construct_impl(JS::Realm&, CSSPerspectiveValue);
28+
29+
virtual ~CSSPerspective() override;
30+
31+
virtual WebIDL::ExceptionOr<Utf16String> to_string() const override;
32+
33+
virtual WebIDL::ExceptionOr<GC::Ref<Geometry::DOMMatrix>> to_matrix() const override;
34+
35+
CSSPerspectiveValue length() const;
36+
WebIDL::ExceptionOr<void> set_length(CSSPerspectiveValue value);
37+
38+
virtual void set_is_2d(bool value) override;
39+
40+
private:
41+
explicit CSSPerspective(JS::Realm&, CSSPerspectiveValueInternal);
42+
43+
virtual void initialize(JS::Realm&) override;
44+
virtual void visit_edges(Visitor&) override;
45+
46+
CSSPerspectiveValueInternal m_length;
47+
};
48+
49+
}
50+
51+
class CSSPerspective {
52+
};
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
#import <CSS/CSSKeywordValue.idl>
2+
#import <CSS/CSSNumericValue.idl>
3+
#import <CSS/CSSTransformComponent.idl>
4+
5+
typedef (CSSNumericValue or CSSKeywordish) CSSPerspectiveValue;
6+
7+
// https://drafts.css-houdini.org/css-typed-om-1/#cssperspective
8+
[Exposed=(Window, Worker, PaintWorklet, LayoutWorklet)]
9+
interface CSSPerspective : CSSTransformComponent {
10+
constructor(CSSPerspectiveValue length);
11+
attribute CSSPerspectiveValue length;
12+
};

Libraries/LibWeb/Forward.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,7 @@ class CSSNumericArray;
258258
class CSSNumericValue;
259259
class CSSPageRule;
260260
class CSSPageDescriptors;
261+
class CSSPerspective;
261262
class CSSPropertyRule;
262263
class CSSRotate;
263264
class CSSRule;

Libraries/LibWeb/idl_files.cmake

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ libweb_js_bindings(CSS/CSSNumericArray)
5353
libweb_js_bindings(CSS/CSSNumericValue)
5454
libweb_js_bindings(CSS/CSSPageRule)
5555
libweb_js_bindings(CSS/CSSPageDescriptors)
56+
libweb_js_bindings(CSS/CSSPerspective)
5657
libweb_js_bindings(CSS/CSSPropertyRule)
5758
libweb_js_bindings(CSS/CSSRotate)
5859
libweb_js_bindings(CSS/CSSRule)

Tests/LibWeb/Text/expected/all-window-properties.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ CSSNumericArray
6565
CSSNumericValue
6666
CSSPageDescriptors
6767
CSSPageRule
68+
CSSPerspective
6869
CSSPropertyRule
6970
CSSRotate
7071
CSSRule

Tests/LibWeb/Text/expected/wpt-import/css/css-typed-om/idlharness.txt

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@ Harness status: OK
22

33
Found 545 tests
44

5-
346 Pass
6-
199 Fail
5+
353 Pass
6+
192 Fail
77
Pass idl_test setup
88
Pass idl_test validation
99
Pass Partial interface Element: original interface defined
@@ -364,13 +364,13 @@ Fail Stringification of skewY
364364
Fail CSSSkewY interface: skewY must inherit property "ay" with the proper type
365365
Fail CSSTransformComponent interface: skewY must inherit property "is2D" with the proper type
366366
Fail CSSTransformComponent interface: skewY must inherit property "toMatrix()" with the proper type
367-
Fail CSSPerspective interface: existence and properties of interface object
368-
Fail CSSPerspective interface object length
369-
Fail CSSPerspective interface object name
370-
Fail CSSPerspective interface: existence and properties of interface prototype object
371-
Fail CSSPerspective interface: existence and properties of interface prototype object's "constructor" property
372-
Fail CSSPerspective interface: existence and properties of interface prototype object's @@unscopables property
373-
Fail CSSPerspective interface: attribute length
367+
Pass CSSPerspective interface: existence and properties of interface object
368+
Pass CSSPerspective interface object length
369+
Pass CSSPerspective interface object name
370+
Pass CSSPerspective interface: existence and properties of interface prototype object
371+
Pass CSSPerspective interface: existence and properties of interface prototype object's "constructor" property
372+
Pass CSSPerspective interface: existence and properties of interface prototype object's @@unscopables property
373+
Pass CSSPerspective interface: attribute length
374374
Fail CSSPerspective must be primary interface of perspective
375375
Fail Stringification of perspective
376376
Fail CSSPerspective interface: perspective must inherit property "length" with the proper type
Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,33 @@
1-
Harness status: Error
1+
Harness status: OK
22

3-
Found 2 tests
3+
Found 28 tests
44

5-
2 Fail
5+
28 Fail
66
Fail Normalizing a matrix() returns a CSSMatrixComponent
7-
Fail Normalizing a matrix3d() returns a CSSMatrixComponent
7+
Fail Normalizing a matrix3d() returns a CSSMatrixComponent
8+
Fail Normalizing a translate() with X returns a CSSTranslate
9+
Fail Normalizing a translate() with X and Y returns a CSSTranslate
10+
Fail Normalizing a translateX() returns a CSSTranslate
11+
Fail Normalizing a translateY() returns a CSSTranslate
12+
Fail Normalizing a translate3d() returns a CSSTranslate
13+
Fail Normalizing a translateZ() returns a CSSTranslate
14+
Fail Normalizing a scale() with one argument returns a CSSScale
15+
Fail Normalizing a scale() with two arguments returns a CSSScale
16+
Fail Normalizing a scaleX() returns a CSSScale
17+
Fail Normalizing a scaleY() returns a CSSScale
18+
Fail Normalizing a scale3d() returns a CSSScale
19+
Fail Normalizing a scaleZ() returns a CSSScale
20+
Fail Normalizing a rotate() returns a CSSRotate
21+
Fail Normalizing a rotate3d() returns a CSSRotate
22+
Fail Normalizing a rotateX() returns a CSSRotate
23+
Fail Normalizing a rotateY() returns a CSSRotate
24+
Fail Normalizing a rotateZ() returns a CSSRotate
25+
Fail Normalizing a skew() with only X returns a CSSSkew
26+
Fail Normalizing a skew() with X and Y which is 0 value returns a CSSSkew
27+
Fail Normalizing a skew() with X and Y returns a CSSSkew
28+
Fail Normalizing a skewX() returns a CSSSkewX
29+
Fail Normalizing a skewY() returns a CSSSkewY
30+
Fail Normalizing a perspective() returns a CSSPerspective
31+
Fail Normalizing a perspective(none) returns a CSSPerspective
32+
Fail Normalizing a <transform-list> returns a CSSTransformValue containing all the transforms
33+
Fail Normalizing transforms with calc values contains CSSMathValues

Tests/LibWeb/Text/expected/wpt-import/css/css-typed-om/stylevalue-subclasses/cssPerspective.tentative.txt

Lines changed: 26 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -2,29 +2,29 @@ Harness status: OK
22

33
Found 25 tests
44

5-
25 Fail
6-
Fail Constructing a CSSPerspective with a keyword other than none (string) throws a TypeError
7-
Fail Constructing a CSSPerspective with a keyword other than none (CSSKeywordValue) throws a TypeError
8-
Fail Constructing a CSSPerspective with a double throws a TypeError
9-
Fail Constructing a CSSPerspective with a unitless zero throws a TypeError
10-
Fail Constructing a CSSPerspective with a string length throws a TypeError
11-
Fail Constructing a CSSPerspective with a number CSSUnitValue throws a TypeError
12-
Fail Constructing a CSSPerspective with a time dimension CSSUnitValue throws a TypeError
13-
Fail Constructing a CSSPerspective with a CSSMathValue of angle type throws a TypeError
14-
Fail Updating CSSPerspective.length with a keyword other than none (string) throws a TypeError
15-
Fail Updating CSSPerspective.length with a keyword other than none (CSSKeywordValue) throws a TypeError
16-
Fail Updating CSSPerspective.length with a double throws a TypeError
17-
Fail Updating CSSPerspective.length with a unitless zero throws a TypeError
18-
Fail Updating CSSPerspective.length with a string length throws a TypeError
19-
Fail Updating CSSPerspective.length with a number CSSUnitValue throws a TypeError
20-
Fail Updating CSSPerspective.length with a time dimension CSSUnitValue throws a TypeError
21-
Fail Updating CSSPerspective.length with a CSSMathValue of angle type throws a TypeError
22-
Fail CSSPerspective can be constructed from a length CSSUnitValue
23-
Fail CSSPerspective.length can be updated to a length CSSUnitValue
24-
Fail CSSPerspective can be constructed from a CSSMathValue of length type
25-
Fail CSSPerspective.length can be updated to a CSSMathValue of length type
26-
Fail CSSPerspective can be constructed from none (CSSKeywordValue)
27-
Fail CSSPerspective.length can be updated to none (CSSKeywordValue)
28-
Fail CSSPerspective can be constructed from none (string)
29-
Fail CSSPerspective.length can be updated to none (string)
30-
Fail Modifying CSSPerspective.is2D is a no-op
5+
25 Pass
6+
Pass Constructing a CSSPerspective with a keyword other than none (string) throws a TypeError
7+
Pass Constructing a CSSPerspective with a keyword other than none (CSSKeywordValue) throws a TypeError
8+
Pass Constructing a CSSPerspective with a double throws a TypeError
9+
Pass Constructing a CSSPerspective with a unitless zero throws a TypeError
10+
Pass Constructing a CSSPerspective with a string length throws a TypeError
11+
Pass Constructing a CSSPerspective with a number CSSUnitValue throws a TypeError
12+
Pass Constructing a CSSPerspective with a time dimension CSSUnitValue throws a TypeError
13+
Pass Constructing a CSSPerspective with a CSSMathValue of angle type throws a TypeError
14+
Pass Updating CSSPerspective.length with a keyword other than none (string) throws a TypeError
15+
Pass Updating CSSPerspective.length with a keyword other than none (CSSKeywordValue) throws a TypeError
16+
Pass Updating CSSPerspective.length with a double throws a TypeError
17+
Pass Updating CSSPerspective.length with a unitless zero throws a TypeError
18+
Pass Updating CSSPerspective.length with a string length throws a TypeError
19+
Pass Updating CSSPerspective.length with a number CSSUnitValue throws a TypeError
20+
Pass Updating CSSPerspective.length with a time dimension CSSUnitValue throws a TypeError
21+
Pass Updating CSSPerspective.length with a CSSMathValue of angle type throws a TypeError
22+
Pass CSSPerspective can be constructed from a length CSSUnitValue
23+
Pass CSSPerspective.length can be updated to a length CSSUnitValue
24+
Pass CSSPerspective can be constructed from a CSSMathValue of length type
25+
Pass CSSPerspective.length can be updated to a CSSMathValue of length type
26+
Pass CSSPerspective can be constructed from none (CSSKeywordValue)
27+
Pass CSSPerspective.length can be updated to none (CSSKeywordValue)
28+
Pass CSSPerspective can be constructed from none (string)
29+
Pass CSSPerspective.length can be updated to none (string)
30+
Pass Modifying CSSPerspective.is2D is a no-op

0 commit comments

Comments
 (0)