diff --git a/Libraries/LibWeb/CMakeLists.txt b/Libraries/LibWeb/CMakeLists.txt index 207f95eb9d1..2c1d0755969 100644 --- a/Libraries/LibWeb/CMakeLists.txt +++ b/Libraries/LibWeb/CMakeLists.txt @@ -149,6 +149,7 @@ set(SOURCES CSS/StyleValues/RadialGradientStyleValue.cpp CSS/StyleValues/RectStyleValue.cpp CSS/StyleValues/RotationStyleValue.cpp + CSS/StyleValues/ScaleStyleValue.cpp CSS/StyleValues/ShadowStyleValue.cpp CSS/StyleValues/ShorthandStyleValue.cpp CSS/StyleValues/StyleValueList.cpp diff --git a/Libraries/LibWeb/CSS/CSSStyleValue.cpp b/Libraries/LibWeb/CSS/CSSStyleValue.cpp index fde6552afd2..f3eeb55adb3 100644 --- a/Libraries/LibWeb/CSS/CSSStyleValue.cpp +++ b/Libraries/LibWeb/CSS/CSSStyleValue.cpp @@ -48,6 +48,7 @@ #include #include #include +#include #include #include #include @@ -295,6 +296,12 @@ RotationStyleValue const& CSSStyleValue::as_rotation() const return static_cast(*this); } +ScaleStyleValue const& CSSStyleValue::as_scale() const +{ + VERIFY(is_scale()); + return static_cast(*this); +} + ScrollbarGutterStyleValue const& CSSStyleValue::as_scrollbar_gutter() const { VERIFY(is_scrollbar_gutter()); diff --git a/Libraries/LibWeb/CSS/CSSStyleValue.h b/Libraries/LibWeb/CSS/CSSStyleValue.h index b4a29721d63..e8cf81690f0 100644 --- a/Libraries/LibWeb/CSS/CSSStyleValue.h +++ b/Libraries/LibWeb/CSS/CSSStyleValue.h @@ -125,6 +125,7 @@ public: Rect, Resolution, Rotation, + Scale, ScrollbarGutter, Shadow, Shorthand, @@ -295,6 +296,10 @@ public: RotationStyleValue const& as_rotation() const; RotationStyleValue& as_rotation() { return const_cast(const_cast(*this).as_rotation()); } + bool is_scale() const { return type() == Type::Scale; } + ScaleStyleValue const& as_scale() const; + ScaleStyleValue& as_scale() { return const_cast(const_cast(*this).as_scale()); } + bool is_scrollbar_gutter() const { return type() == Type::ScrollbarGutter; } ScrollbarGutterStyleValue const& as_scrollbar_gutter() const; ScrollbarGutterStyleValue& as_scrollbar_gutter() { return const_cast(const_cast(*this).as_scrollbar_gutter()); } diff --git a/Libraries/LibWeb/CSS/ComputedValues.h b/Libraries/LibWeb/CSS/ComputedValues.h index d712f30e7de..360df02495c 100644 --- a/Libraries/LibWeb/CSS/ComputedValues.h +++ b/Libraries/LibWeb/CSS/ComputedValues.h @@ -509,6 +509,7 @@ public: CSS::TransformOrigin const& transform_origin() const { return m_noninherited.transform_origin; } Optional const& rotate() const { return m_noninherited.rotate; } Optional const& translate() const { return m_noninherited.translate; } + Optional const& scale() const { return m_noninherited.scale; } Gfx::FontCascadeList const& font_list() const { return *m_inherited.font_list; } CSSPixels font_size() const { return m_inherited.font_size; } @@ -686,6 +687,7 @@ protected: CSS::UnicodeBidi unicode_bidi { InitialValues::unicode_bidi() }; Optional rotate; Optional translate; + Optional scale; Optional mask; CSS::MaskType mask_type { InitialValues::mask_type() }; @@ -799,6 +801,7 @@ public: void set_justify_self(CSS::JustifySelf value) { m_noninherited.justify_self = value; } void set_box_shadow(Vector&& value) { m_noninherited.box_shadow = move(value); } void set_rotate(CSS::Transformation value) { m_noninherited.rotate = move(value); } + void set_scale(CSS::Transformation value) { m_noninherited.scale = move(value); } void set_transformations(Vector value) { m_noninherited.transformations = move(value); } void set_transform_box(CSS::TransformBox value) { m_noninherited.transform_box = value; } void set_transform_origin(CSS::TransformOrigin value) { m_noninherited.transform_origin = value; } diff --git a/Libraries/LibWeb/CSS/Parser/Parser.cpp b/Libraries/LibWeb/CSS/Parser/Parser.cpp index dc9f7839912..0d85225db8f 100644 --- a/Libraries/LibWeb/CSS/Parser/Parser.cpp +++ b/Libraries/LibWeb/CSS/Parser/Parser.cpp @@ -68,6 +68,7 @@ #include #include #include +#include #include #include #include @@ -6956,6 +6957,33 @@ RefPtr Parser::parse_translate_value(TokenStream& return TranslationStyleValue::create(maybe_x.release_value(), maybe_y.release_value()); } +RefPtr Parser::parse_scale_value(TokenStream& tokens) +{ + if (tokens.remaining_token_count() == 1) { + // "none" + if (auto none = parse_all_as_single_keyword_value(tokens, Keyword::None)) + return none; + } + + auto transaction = tokens.begin_transaction(); + + auto maybe_x = parse_number_percentage(tokens); + if (!maybe_x.has_value()) + return nullptr; + + if (!tokens.has_next_token()) { + transaction.commit(); + return ScaleStyleValue::create(maybe_x.value(), maybe_x.value()); + } + + auto maybe_y = parse_number_percentage(tokens); + if (!maybe_y.has_value()) + return nullptr; + + transaction.commit(); + return ScaleStyleValue::create(maybe_x.release_value(), maybe_y.release_value()); +} + Optional Parser::parse_fit_content(Vector const& component_values) { // https://www.w3.org/TR/css-grid-2/#valdef-grid-template-columns-fit-content @@ -7977,6 +8005,10 @@ Parser::ParseErrorOr> Parser::parse_css_value(Prope if (auto parsed_value = parse_translate_value(tokens); parsed_value && !tokens.has_next_token()) return parsed_value.release_nonnull(); return ParseError::SyntaxError; + case PropertyID::Scale: + if (auto parsed_value = parse_scale_value(tokens); parsed_value && !tokens.has_next_token()) + return parsed_value.release_nonnull(); + return ParseError::SyntaxError; default: break; } diff --git a/Libraries/LibWeb/CSS/Parser/Parser.h b/Libraries/LibWeb/CSS/Parser/Parser.h index c015040534e..4698eeb0687 100644 --- a/Libraries/LibWeb/CSS/Parser/Parser.h +++ b/Libraries/LibWeb/CSS/Parser/Parser.h @@ -345,6 +345,7 @@ private: RefPtr parse_transform_origin_value(TokenStream&); RefPtr parse_transition_value(TokenStream&); RefPtr parse_translate_value(TokenStream&); + RefPtr parse_scale_value(TokenStream&); RefPtr parse_grid_track_size_list(TokenStream&, bool allow_separate_line_name_blocks = false); RefPtr parse_grid_auto_track_sizes(TokenStream&); RefPtr parse_grid_auto_flow_value(TokenStream&); diff --git a/Libraries/LibWeb/CSS/Properties.json b/Libraries/LibWeb/CSS/Properties.json index b3252cae1b8..346c9e4ff68 100644 --- a/Libraries/LibWeb/CSS/Properties.json +++ b/Libraries/LibWeb/CSS/Properties.json @@ -2370,6 +2370,13 @@ "unitless-length" ] }, + "scale": { + "animation-type": "custom", + "inherited": false, + "initial": "none", + "affects-layout": false, + "affects-stacking-context": true + }, "scrollbar-gutter": { "affects-layout": false, "animation-type": "discrete", diff --git a/Libraries/LibWeb/CSS/StyleProperties.cpp b/Libraries/LibWeb/CSS/StyleProperties.cpp index cc8ba99ce04..396a8a98a68 100644 --- a/Libraries/LibWeb/CSS/StyleProperties.cpp +++ b/Libraries/LibWeb/CSS/StyleProperties.cpp @@ -29,6 +29,7 @@ #include #include #include +#include #include #include #include @@ -614,6 +615,20 @@ Optional StyleProperties::translate() const return CSS::Transformation(CSS::TransformFunction::Translate, move(values)); } +Optional StyleProperties::scale() const +{ + auto const& value = property(CSS::PropertyID::Scale); + if (!value.is_scale()) + return {}; + auto const& scale = value.as_scale(); + + Vector values; + values.append(scale.x()); + values.append(scale.y()); + + return CSS::Transformation(CSS::TransformFunction::Scale, move(values)); +} + static Optional length_percentage_for_style_value(CSSStyleValue const& value) { if (value.is_length()) diff --git a/Libraries/LibWeb/CSS/StyleProperties.h b/Libraries/LibWeb/CSS/StyleProperties.h index 6d4b7033edd..4eb45869b40 100644 --- a/Libraries/LibWeb/CSS/StyleProperties.h +++ b/Libraries/LibWeb/CSS/StyleProperties.h @@ -177,6 +177,7 @@ public: CSS::TransformOrigin transform_origin() const; Optional rotate(Layout::Node const&) const; Optional translate() const; + Optional scale() const; Optional mask_type() const; Color stop_color() const; diff --git a/Libraries/LibWeb/CSS/StyleValues/ScaleStyleValue.cpp b/Libraries/LibWeb/CSS/StyleValues/ScaleStyleValue.cpp new file mode 100644 index 00000000000..0f53d2a3a36 --- /dev/null +++ b/Libraries/LibWeb/CSS/StyleValues/ScaleStyleValue.cpp @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2024, Andreas Kling + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include +#include + +namespace Web::CSS { + +// https://www.w3.org/TR/2021/WD-css-transforms-2-20211109/#individual-transform-serialization +String ScaleStyleValue::to_string() const +{ + auto resolve_to_string = [](NumberPercentage const& value) -> String { + if (value.is_number()) { + return MUST(String::formatted("{}", value.number().value())); + } + if (value.is_percentage()) { + return MUST(String::formatted("{}", value.percentage().value() / 100.0)); + } + return value.to_string(); + }; + + auto x_value = resolve_to_string(m_properties.x); + auto y_value = resolve_to_string(m_properties.y); + + StringBuilder builder; + builder.append(x_value); + if (x_value != y_value) { + builder.append(" "sv); + builder.append(y_value); + } + return builder.to_string_without_validation(); +} + +} diff --git a/Libraries/LibWeb/CSS/StyleValues/ScaleStyleValue.h b/Libraries/LibWeb/CSS/StyleValues/ScaleStyleValue.h new file mode 100644 index 00000000000..f3b36dde99f --- /dev/null +++ b/Libraries/LibWeb/CSS/StyleValues/ScaleStyleValue.h @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2024, Andreas Kling + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include +#include + +namespace Web::CSS { + +class ScaleStyleValue : public StyleValueWithDefaultOperators { +public: + static ValueComparingNonnullRefPtr create(NumberPercentage x, NumberPercentage y) + { + return adopt_ref(*new (nothrow) ScaleStyleValue(move(x), move(y))); + } + + virtual ~ScaleStyleValue() override = default; + + NumberPercentage const& x() const { return m_properties.x; } + NumberPercentage const& y() const { return m_properties.y; } + + virtual String to_string() const override; + + bool properties_equal(ScaleStyleValue const& other) const { return m_properties == other.m_properties; } + +private: + explicit ScaleStyleValue( + NumberPercentage x, + NumberPercentage y) + : StyleValueWithDefaultOperators(Type::Scale) + , m_properties { + .x = move(x), + .y = move(y), + } + { + } + + struct Properties { + NumberPercentage x; + NumberPercentage y; + bool operator==(Properties const&) const = default; + } m_properties; +}; + +} diff --git a/Libraries/LibWeb/Forward.h b/Libraries/LibWeb/Forward.h index 48f4fea8ead..46383286d9b 100644 --- a/Libraries/LibWeb/Forward.h +++ b/Libraries/LibWeb/Forward.h @@ -205,6 +205,7 @@ class Resolution; class ResolutionOrCalculated; class ResolutionStyleValue; class RotationStyleValue; +class ScaleStyleValue; class Screen; class ScreenOrientation; class ScrollbarGutterStyleValue; diff --git a/Libraries/LibWeb/Layout/Node.cpp b/Libraries/LibWeb/Layout/Node.cpp index 943f421b02b..ec54f02dd4c 100644 --- a/Libraries/LibWeb/Layout/Node.cpp +++ b/Libraries/LibWeb/Layout/Node.cpp @@ -86,6 +86,8 @@ bool Node::can_contain_boxes_with_position_absolute() const return true; if (computed_values().rotate().has_value()) return true; + if (computed_values().scale().has_value()) + return true; return false; } @@ -185,6 +187,9 @@ bool Node::establishes_stacking_context() const if (computed_values().rotate().has_value()) return true; + if (computed_values().scale().has_value()) + return true; + // Element that is a child of a flex container, with z-index value other than auto. if (parent() && parent()->display().is_flex_inside() && computed_values().z_index().has_value()) return true; @@ -725,6 +730,9 @@ void NodeWithStyle::apply_style(const CSS::StyleProperties& computed_style) if (auto translate_value = computed_style.translate(); translate_value.has_value()) computed_values.set_translate(translate_value.release_value()); + if (auto scale_value = computed_style.scale(); scale_value.has_value()) + computed_values.set_scale(scale_value.release_value()); + computed_values.set_transformations(computed_style.transformations()); if (auto transform_box = computed_style.transform_box(); transform_box.has_value()) computed_values.set_transform_box(transform_box.value()); diff --git a/Libraries/LibWeb/Layout/Node.h b/Libraries/LibWeb/Layout/Node.h index b4f98110b03..8e751ca6153 100644 --- a/Libraries/LibWeb/Layout/Node.h +++ b/Libraries/LibWeb/Layout/Node.h @@ -186,6 +186,8 @@ public: return true; if (computed_values().translate().has_value()) return true; + if (computed_values().scale().has_value()) + return true; return false; } diff --git a/Libraries/LibWeb/Painting/PaintableBox.cpp b/Libraries/LibWeb/Painting/PaintableBox.cpp index c8791587198..cffeb011031 100644 --- a/Libraries/LibWeb/Painting/PaintableBox.cpp +++ b/Libraries/LibWeb/Painting/PaintableBox.cpp @@ -1130,12 +1130,15 @@ void PaintableBox::resolve_paint_properties() auto const& transformations = computed_values.transformations(); auto const& translate = computed_values.translate(); auto const& rotate = computed_values.rotate(); - if (!transformations.is_empty() || translate.has_value() || rotate.has_value()) { + auto const& scale = computed_values.scale(); + if (!transformations.is_empty() || translate.has_value() || rotate.has_value() || scale.has_value()) { auto matrix = Gfx::FloatMatrix4x4::identity(); if (translate.has_value()) matrix = matrix * translate->to_matrix(*this).release_value(); if (rotate.has_value()) matrix = matrix * rotate->to_matrix(*this).release_value(); + if (scale.has_value()) + matrix = matrix * scale->to_matrix(*this).release_value(); for (auto const& transform : transformations) matrix = matrix * transform.to_matrix(*this).release_value(); set_transform(matrix); diff --git a/Libraries/LibWeb/Painting/PaintableBox.h b/Libraries/LibWeb/Painting/PaintableBox.h index 25bb4d97b78..8b3c8437649 100644 --- a/Libraries/LibWeb/Painting/PaintableBox.h +++ b/Libraries/LibWeb/Painting/PaintableBox.h @@ -137,6 +137,8 @@ public: return true; if (computed_values().translate().has_value()) return true; + if (computed_values().scale().has_value()) + return true; return false; } diff --git a/Tests/LibWeb/Text/expected/css/CSSStyleDeclaration-all-supported-properties-and-default-values.txt b/Tests/LibWeb/Text/expected/css/CSSStyleDeclaration-all-supported-properties-and-default-values.txt index b0b29e54c83..41ac64ef835 100644 --- a/Tests/LibWeb/Text/expected/css/CSSStyleDeclaration-all-supported-properties-and-default-values.txt +++ b/Tests/LibWeb/Text/expected/css/CSSStyleDeclaration-all-supported-properties-and-default-values.txt @@ -1,6 +1,6 @@ All supported properties and their default values exposed from CSSStyleDeclaration from getComputedStyle: 'cssText': '' -'length': '205' +'length': '206' 'parentRule': 'null' 'cssFloat': 'none' 'WebkitAlignContent': 'normal' @@ -486,6 +486,7 @@ All supported properties and their default values exposed from CSSStyleDeclarati 'row-gap': 'normal' 'rx': 'auto' 'ry': 'auto' +'scale': 'none' 'scrollbarGutter': 'auto' 'scrollbar-gutter': 'auto' 'scrollbarWidth': 'auto' 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 5f6b0f5553d..1354e1e7336 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 @@ -180,31 +180,32 @@ All properties associated with getComputedStyle(document.body): "177": "row-gap", "178": "rx", "179": "ry", - "180": "scrollbar-gutter", - "181": "scrollbar-width", - "182": "stop-color", - "183": "stop-opacity", - "184": "table-layout", - "185": "text-decoration-color", - "186": "text-decoration-style", - "187": "text-decoration-thickness", - "188": "text-overflow", - "189": "top", - "190": "transform", - "191": "transform-box", - "192": "transform-origin", - "193": "transition-delay", - "194": "transition-duration", - "195": "transition-property", - "196": "transition-timing-function", - "197": "translate", - "198": "unicode-bidi", - "199": "user-select", - "200": "vertical-align", - "201": "width", - "202": "x", - "203": "y", - "204": "z-index" + "180": "scale", + "181": "scrollbar-gutter", + "182": "scrollbar-width", + "183": "stop-color", + "184": "stop-opacity", + "185": "table-layout", + "186": "text-decoration-color", + "187": "text-decoration-style", + "188": "text-decoration-thickness", + "189": "text-overflow", + "190": "top", + "191": "transform", + "192": "transform-box", + "193": "transform-origin", + "194": "transition-delay", + "195": "transition-duration", + "196": "transition-property", + "197": "transition-timing-function", + "198": "translate", + "199": "unicode-bidi", + "200": "user-select", + "201": "vertical-align", + "202": "width", + "203": "x", + "204": "y", + "205": "z-index" } All properties associated with document.body.style by default: {} diff --git a/Tests/LibWeb/Text/expected/css/getComputedStyle-print-all.txt b/Tests/LibWeb/Text/expected/css/getComputedStyle-print-all.txt index 1d91f41e986..dc729509efc 100644 --- a/Tests/LibWeb/Text/expected/css/getComputedStyle-print-all.txt +++ b/Tests/LibWeb/Text/expected/css/getComputedStyle-print-all.txt @@ -178,6 +178,7 @@ rotate: none row-gap: normal rx: auto ry: auto +scale: none scrollbar-gutter: auto scrollbar-width: auto stop-color: rgb(0, 0, 0) diff --git a/Tests/LibWeb/Text/expected/wpt-import/css/css-transforms/parsing/scale-parsing-computed.txt b/Tests/LibWeb/Text/expected/wpt-import/css/css-transforms/parsing/scale-parsing-computed.txt index e68f6c0f28c..3e801e92429 100644 --- a/Tests/LibWeb/Text/expected/wpt-import/css/css-transforms/parsing/scale-parsing-computed.txt +++ b/Tests/LibWeb/Text/expected/wpt-import/css/css-transforms/parsing/scale-parsing-computed.txt @@ -6,25 +6,26 @@ Rerun Found 22 tests -22 Fail +13 Pass +9 Fail Details -Result Test Name MessageFail Property scale value 'none' -Fail Property scale value '1' -Fail Property scale value '1%' -Fail Property scale value '100' -Fail Property scale value '100%' -Fail Property scale value '100 100' -Fail Property scale value '100% 100%' +Result Test Name MessagePass Property scale value 'none' +Pass Property scale value '1' +Pass Property scale value '1%' +Pass Property scale value '100' +Pass Property scale value '100%' +Pass Property scale value '100 100' +Pass Property scale value '100% 100%' Fail Property scale value '100 100 1' Fail Property scale value '100% 100% 1' -Fail Property scale value '-100' -Fail Property scale value '-100%' -Fail Property scale value '-100 -100' -Fail Property scale value '-100% -100%' +Pass Property scale value '-100' +Pass Property scale value '-100%' +Pass Property scale value '-100 -100' +Pass Property scale value '-100% -100%' Fail Property scale value '-100 -100 1' Fail Property scale value '-100% -100% 1' -Fail Property scale value '100 200' -Fail Property scale value '100% 200%' +Pass Property scale value '100 200' +Pass Property scale value '100% 200%' Fail Property scale value '100 200 1' Fail Property scale value '100% 200% 1' Fail Property scale value '100 200 300' diff --git a/Tests/LibWeb/Text/expected/wpt-import/css/css-transforms/parsing/scale-parsing-valid.txt b/Tests/LibWeb/Text/expected/wpt-import/css/css-transforms/parsing/scale-parsing-valid.txt index df230dc5ee6..f641c6ed6da 100644 --- a/Tests/LibWeb/Text/expected/wpt-import/css/css-transforms/parsing/scale-parsing-valid.txt +++ b/Tests/LibWeb/Text/expected/wpt-import/css/css-transforms/parsing/scale-parsing-valid.txt @@ -6,25 +6,26 @@ Rerun Found 22 tests -22 Fail +13 Pass +9 Fail Details -Result Test Name MessageFail e.style['scale'] = "none" should set the property value -Fail e.style['scale'] = "1" should set the property value -Fail e.style['scale'] = "1%" should set the property value -Fail e.style['scale'] = "100" should set the property value -Fail e.style['scale'] = "100%" should set the property value -Fail e.style['scale'] = "100 100" should set the property value -Fail e.style['scale'] = "100% 100%" should set the property value +Result Test Name MessagePass e.style['scale'] = "none" should set the property value +Pass e.style['scale'] = "1" should set the property value +Pass e.style['scale'] = "1%" should set the property value +Pass e.style['scale'] = "100" should set the property value +Pass e.style['scale'] = "100%" should set the property value +Pass e.style['scale'] = "100 100" should set the property value +Pass e.style['scale'] = "100% 100%" should set the property value Fail e.style['scale'] = "100 100 1" should set the property value Fail e.style['scale'] = "100% 100% 1" should set the property value -Fail e.style['scale'] = "-100" should set the property value -Fail e.style['scale'] = "-100%" should set the property value -Fail e.style['scale'] = "-100 -100" should set the property value -Fail e.style['scale'] = "-100% -100%" should set the property value +Pass e.style['scale'] = "-100" should set the property value +Pass e.style['scale'] = "-100%" should set the property value +Pass e.style['scale'] = "-100 -100" should set the property value +Pass e.style['scale'] = "-100% -100%" should set the property value Fail e.style['scale'] = "-100 -100 1" should set the property value Fail e.style['scale'] = "-100% -100% 1" should set the property value -Fail e.style['scale'] = "100 200" should set the property value -Fail e.style['scale'] = "100% 200%" should set the property value +Pass e.style['scale'] = "100 200" should set the property value +Pass e.style['scale'] = "100% 200%" should set the property value Fail e.style['scale'] = "100 200 1" should set the property value Fail e.style['scale'] = "100% 200% 1" should set the property value Fail e.style['scale'] = "100 200 300" should set the property value