From eb21ea890ce39598f1e8b652055aa379bc4dbf1e Mon Sep 17 00:00:00 2001 From: Psychpsyo Date: Mon, 10 Nov 2025 15:10:38 +0100 Subject: [PATCH] LibWeb: Implement CSS perspective property --- Libraries/LibWeb/CSS/ComputedProperties.cpp | 14 +++++++++ Libraries/LibWeb/CSS/ComputedProperties.h | 1 + Libraries/LibWeb/CSS/ComputedValues.h | 3 ++ Libraries/LibWeb/CSS/Properties.json | 11 +++++++ Libraries/LibWeb/Layout/Node.cpp | 12 ++++++++ Libraries/LibWeb/Painting/PaintableBox.cpp | 29 +++++++++++++++++++ Libraries/LibWeb/Painting/PaintableBox.h | 4 +++ Libraries/LibWeb/Painting/StackingContext.cpp | 7 +++++ .../transform3d-rotatex-perspective-002.html | 21 ++++++++++++++ ...eclaration-has-indexed-property-getter.txt | 1 + ...upported-properties-and-default-values.txt | 1 + .../css/getComputedStyle-print-all.txt | 1 + .../css/css-cascade/all-prop-revert-layer.txt | 5 ++-- 13 files changed, 108 insertions(+), 2 deletions(-) create mode 100644 Tests/LibWeb/Ref/input/wpt-import/css/css-transforms/transform3d-rotatex-perspective-002.html diff --git a/Libraries/LibWeb/CSS/ComputedProperties.cpp b/Libraries/LibWeb/CSS/ComputedProperties.cpp index 724e7392a8e..c5afa882103 100644 --- a/Libraries/LibWeb/CSS/ComputedProperties.cpp +++ b/Libraries/LibWeb/CSS/ComputedProperties.cpp @@ -714,6 +714,20 @@ TransformBox ComputedProperties::transform_box() const return keyword_to_transform_box(value.to_keyword()).release_value(); } +Optional ComputedProperties::perspective() const +{ + auto const& value = property(PropertyID::Perspective); + if (value.is_keyword() && value.to_keyword() == Keyword::None) + return {}; + + if (value.is_length()) + return value.as_length().length().absolute_length_to_px(); + if (value.is_calculated()) + return value.as_calculated().resolve_length({ .length_resolution_context = {} })->absolute_length_to_px(); + + VERIFY_NOT_REACHED(); +} + TransformOrigin ComputedProperties::transform_origin() const { auto length_percentage_with_keywords_resolved = [](StyleValue const& value) -> LengthPercentage { diff --git a/Libraries/LibWeb/CSS/ComputedProperties.h b/Libraries/LibWeb/CSS/ComputedProperties.h index ca6d9620d19..4d035676b89 100644 --- a/Libraries/LibWeb/CSS/ComputedProperties.h +++ b/Libraries/LibWeb/CSS/ComputedProperties.h @@ -210,6 +210,7 @@ public: Optional rotate() const; Optional translate() const; Optional scale() const; + Optional perspective() const; MaskType mask_type() const; float stop_opacity() const; diff --git a/Libraries/LibWeb/CSS/ComputedValues.h b/Libraries/LibWeb/CSS/ComputedValues.h index 1021aedf8d0..d8e9d62e2b3 100644 --- a/Libraries/LibWeb/CSS/ComputedValues.h +++ b/Libraries/LibWeb/CSS/ComputedValues.h @@ -637,6 +637,7 @@ public: Optional const& rotate() const { return m_noninherited.rotate; } Optional const& translate() const { return m_noninherited.translate; } Optional const& scale() const { return m_noninherited.scale; } + Optional const& perspective() const { return m_noninherited.perspective; } Gfx::FontCascadeList const& font_list() const { return *m_inherited.font_list; } CSSPixels font_size() const { return m_inherited.font_size; } @@ -838,6 +839,7 @@ protected: Optional rotate; Optional translate; Optional scale; + Optional perspective; Optional mask; MaskType mask_type { InitialValues::mask_type() }; @@ -990,6 +992,7 @@ public: void set_box_shadow(Vector&& value) { m_noninherited.box_shadow = move(value); } void set_rotate(Transformation value) { m_noninherited.rotate = move(value); } void set_scale(Transformation value) { m_noninherited.scale = move(value); } + void set_perspective(Optional value) { m_noninherited.perspective = move(value); } void set_transformations(Vector value) { m_noninherited.transformations = move(value); } void set_transform_box(TransformBox value) { m_noninherited.transform_box = value; } void set_transform_origin(TransformOrigin value) { m_noninherited.transform_origin = move(value); } diff --git a/Libraries/LibWeb/CSS/Properties.json b/Libraries/LibWeb/CSS/Properties.json index c69ee658203..68e18294b45 100644 --- a/Libraries/LibWeb/CSS/Properties.json +++ b/Libraries/LibWeb/CSS/Properties.json @@ -3215,6 +3215,17 @@ "paint-order" ] }, + "perspective": { + "animation-type": "by-computed-value", + "inherited": false, + "initial": "none", + "valid-identifiers": [ + "none" + ], + "valid-types": [ + "length [0,∞]" + ] + }, "place-content": { "inherited": false, "initial": "normal", diff --git a/Libraries/LibWeb/Layout/Node.cpp b/Libraries/LibWeb/Layout/Node.cpp index 8703b142196..b092dbbf764 100644 --- a/Libraries/LibWeb/Layout/Node.cpp +++ b/Libraries/LibWeb/Layout/Node.cpp @@ -98,6 +98,12 @@ bool Node::can_contain_boxes_with_position_absolute() const if (computed_values().scale().has_value()) return true; + // https://drafts.csswg.org/css-transforms-2/#propdef-perspective + // The use of this property with any value other than 'none' establishes a stacking context. It also establishes + // a containing block for all descendants, just like the 'transform' property does. + if (computed_values().perspective().has_value()) + return true; + // https://drafts.csswg.org/css-contain-2/#containment-types // 4. The layout containment box establishes an absolute positioning containing block and a fixed positioning // containing block. @@ -284,6 +290,11 @@ bool Node::establishes_stacking_context() const if (computed_values.view_transition_name().has_value() || will_change_property(CSS::PropertyID::ViewTransitionName)) return true; + // https://drafts.csswg.org/css-transforms-2/#propdef-perspective + // The use of this property with any value other than 'none' establishes a stacking context. + if (computed_values.perspective().has_value() || will_change_property(CSS::PropertyID::Perspective)) + return true; + return computed_values.opacity() < 1.0f || will_change_property(CSS::PropertyID::Opacity); } @@ -682,6 +693,7 @@ void NodeWithStyle::apply_style(CSS::ComputedProperties const& computed_style) computed_values.set_transformations(computed_style.transformations()); computed_values.set_transform_box(computed_style.transform_box()); computed_values.set_transform_origin(computed_style.transform_origin()); + computed_values.set_perspective(computed_style.perspective()); auto const& transition_delay_property = computed_style.property(CSS::PropertyID::TransitionDelay); if (transition_delay_property.is_time()) { diff --git a/Libraries/LibWeb/Painting/PaintableBox.cpp b/Libraries/LibWeb/Painting/PaintableBox.cpp index 3e00a7c3261..d782902b73f 100644 --- a/Libraries/LibWeb/Painting/PaintableBox.cpp +++ b/Libraries/LibWeb/Painting/PaintableBox.cpp @@ -1535,6 +1535,35 @@ void PaintableBox::resolve_paint_properties() matrix = matrix * transform.to_matrix(*this).release_value(); set_transform(matrix); + // https://drafts.csswg.org/css-transforms-2/#perspective + auto const& perspective = computed_values.perspective(); + if (perspective.has_value()) { + // The perspective matrix is computed as follows: + + // 1. Start with the identity matrix. + m_perspective_matrix = Gfx::FloatMatrix4x4::identity(); + + // 2. Translate by the computed X and Y values of 'perspective-origin' + // FIXME: Implement this. + + // 3. Multiply by the matrix that would be obtained from the 'perspective()' transform function, where the + // length is provided by the value of the perspective property + auto perspective_value = perspective.value(); + + // https://drafts.csswg.org/css-transforms-2/#perspective-property + // As very small values can produce bizarre rendering results and stress the numerical accuracy + // of transform calculations, values less than '1px' must be treated as '1px' for rendering purposes. (This + // clamping does not affect the underlying value, so 'perspective: 0;' in a stylesheet will still serialize back + // as '0'.) + if (perspective_value < 1) + perspective_value = 1; + + m_perspective_matrix = m_perspective_matrix * CSS::Transformation(CSS::TransformFunction::Perspective, Vector({ CSS::TransformValue(CSS::Length::make_px(perspective_value)) })).to_matrix(*this).release_value(); + + // 4. Translate by the negated computed X and Y values of 'perspective-origin' + // FIXME: Implement this. + } + auto const& transform_origin = computed_values.transform_origin(); auto reference_box = transform_box_rect(); auto x = reference_box.left() + transform_origin.x.to_px(layout_node, reference_box.width()); diff --git a/Libraries/LibWeb/Painting/PaintableBox.h b/Libraries/LibWeb/Painting/PaintableBox.h index bd5199f38a8..f396a91b3e3 100644 --- a/Libraries/LibWeb/Painting/PaintableBox.h +++ b/Libraries/LibWeb/Painting/PaintableBox.h @@ -201,6 +201,9 @@ public: void set_transform(Gfx::FloatMatrix4x4 transform) { m_transform = transform; } Gfx::FloatMatrix4x4 const& transform() const { return m_transform; } + void set_perspective_matrix(Gfx::FloatMatrix4x4 perspective_matrix) { m_perspective_matrix = perspective_matrix; } + Gfx::FloatMatrix4x4 const& perspective_matrix() const { return m_perspective_matrix; } + void set_transform_origin(CSSPixelPoint transform_origin) { m_transform_origin = transform_origin; } CSSPixelPoint const& transform_origin() const { return m_transform_origin; } @@ -328,6 +331,7 @@ private: BorderRadiiData m_border_radii_data; Vector m_box_shadow_data; Gfx::FloatMatrix4x4 m_transform { Gfx::FloatMatrix4x4::identity() }; + Gfx::FloatMatrix4x4 m_perspective_matrix { Gfx::FloatMatrix4x4::identity() }; CSSPixelPoint m_transform_origin; Optional m_outline_data; diff --git a/Libraries/LibWeb/Painting/StackingContext.cpp b/Libraries/LibWeb/Painting/StackingContext.cpp index 807581f5023..0f7d4e9ffdd 100644 --- a/Libraries/LibWeb/Painting/StackingContext.cpp +++ b/Libraries/LibWeb/Painting/StackingContext.cpp @@ -306,6 +306,13 @@ void StackingContext::paint(DisplayListRecordingContext& context) const auto source_paintable_rect = context.enclosing_device_rect(paintable_box().absolute_paint_rect()).to_type(); auto transform_matrix = paintable_box().transform(); + // https://drafts.csswg.org/css-transforms-2/#perspective + // Second, the 'perspective' and 'perspective-origin' properties can be applied to an element to influence the + // rendering of its 3d-transformed children, giving them a shared perspective that provides the impression of + // them living in the same three-dimensional scene. + if (auto const* parent = as_if(paintable_box().parent())) + transform_matrix = parent->perspective_matrix() * transform_matrix; + auto transform_origin = paintable_box().transform_origin().to_type(); Gfx::CompositingAndBlendingOperator compositing_and_blending_operator = mix_blend_mode_to_compositing_and_blending_operator(paintable_box().computed_values().mix_blend_mode()); diff --git a/Tests/LibWeb/Ref/input/wpt-import/css/css-transforms/transform3d-rotatex-perspective-002.html b/Tests/LibWeb/Ref/input/wpt-import/css/css-transforms/transform3d-rotatex-perspective-002.html new file mode 100644 index 00000000000..7bd1e35f563 --- /dev/null +++ b/Tests/LibWeb/Ref/input/wpt-import/css/css-transforms/transform3d-rotatex-perspective-002.html @@ -0,0 +1,21 @@ + + + + CSS Test (Transforms): rotatex() and 'perspective' + + + + + + + + + +
+
+
+ + diff --git a/Tests/LibWeb/Text/expected/css/CSSStyleDeclaration-has-indexed-property-getter.txt b/Tests/LibWeb/Text/expected/css/CSSStyleDeclaration-has-indexed-property-getter.txt index 1eee28bd9f6..9701f2d1c84 100644 --- a/Tests/LibWeb/Text/expected/css/CSSStyleDeclaration-has-indexed-property-getter.txt +++ b/Tests/LibWeb/Text/expected/css/CSSStyleDeclaration-has-indexed-property-getter.txt @@ -244,6 +244,7 @@ All properties associated with getComputedStyle(document.body): "padding-left", "padding-right", "padding-top", + "perspective", "position", "position-anchor", "position-area", diff --git a/Tests/LibWeb/Text/expected/css/CSSStyleProperties-all-supported-properties-and-default-values.txt b/Tests/LibWeb/Text/expected/css/CSSStyleProperties-all-supported-properties-and-default-values.txt index 847010e64dc..4bd96d4f85d 100644 --- a/Tests/LibWeb/Text/expected/css/CSSStyleProperties-all-supported-properties-and-default-values.txt +++ b/Tests/LibWeb/Text/expected/css/CSSStyleProperties-all-supported-properties-and-default-values.txt @@ -663,6 +663,7 @@ All supported properties and their default values exposed from CSSStylePropertie 'padding-top': '0px' 'paintOrder': 'normal' 'paint-order': 'normal' +'perspective': 'none' 'placeContent': 'normal' 'place-content': 'normal' 'placeItems': 'normal legacy' diff --git a/Tests/LibWeb/Text/expected/css/getComputedStyle-print-all.txt b/Tests/LibWeb/Text/expected/css/getComputedStyle-print-all.txt index adbc649d9c5..4a48f8cf407 100644 --- a/Tests/LibWeb/Text/expected/css/getComputedStyle-print-all.txt +++ b/Tests/LibWeb/Text/expected/css/getComputedStyle-print-all.txt @@ -242,6 +242,7 @@ padding-inline-start: 0px padding-left: 0px padding-right: 0px padding-top: 0px +perspective: none position: static position-anchor: auto position-area: none diff --git a/Tests/LibWeb/Text/expected/wpt-import/css/css-cascade/all-prop-revert-layer.txt b/Tests/LibWeb/Text/expected/wpt-import/css/css-cascade/all-prop-revert-layer.txt index c066f1b75e0..1ba2c100b91 100644 --- a/Tests/LibWeb/Text/expected/wpt-import/css/css-cascade/all-prop-revert-layer.txt +++ b/Tests/LibWeb/Text/expected/wpt-import/css/css-cascade/all-prop-revert-layer.txt @@ -1,8 +1,8 @@ Harness status: OK -Found 269 tests +Found 270 tests -263 Pass +264 Pass 6 Fail Pass accent-color Pass border-collapse @@ -231,6 +231,7 @@ Pass padding-inline-start Pass padding-left Pass padding-right Pass padding-top +Pass perspective Pass position Pass r Pass right