diff --git a/Meta/gn/secondary/Userland/Libraries/LibWeb/CSS/StyleValues/BUILD.gn b/Meta/gn/secondary/Userland/Libraries/LibWeb/CSS/StyleValues/BUILD.gn index 40ce7fdb276..4e1d294aebf 100644 --- a/Meta/gn/secondary/Userland/Libraries/LibWeb/CSS/StyleValues/BUILD.gn +++ b/Meta/gn/secondary/Userland/Libraries/LibWeb/CSS/StyleValues/BUILD.gn @@ -38,6 +38,7 @@ source_set("StyleValues") { "PositionStyleValue.cpp", "RadialGradientStyleValue.cpp", "RectStyleValue.cpp", + "RotationStyleValue.cpp", "ShadowStyleValue.cpp", "ShorthandStyleValue.cpp", "StyleValueList.cpp", diff --git a/Tests/LibWeb/Text/expected/css/getComputedStyle-print-all.txt b/Tests/LibWeb/Text/expected/css/getComputedStyle-print-all.txt index 91999e359bc..6ec0f4293be 100644 --- a/Tests/LibWeb/Text/expected/css/getComputedStyle-print-all.txt +++ b/Tests/LibWeb/Text/expected/css/getComputedStyle-print-all.txt @@ -168,6 +168,7 @@ padding-top: 0px position: static r: 0px right: auto +rotate: none row-gap: auto rx: auto ry: auto diff --git a/Userland/Libraries/LibWeb/CMakeLists.txt b/Userland/Libraries/LibWeb/CMakeLists.txt index 205decf3a0a..882b87f560a 100644 --- a/Userland/Libraries/LibWeb/CMakeLists.txt +++ b/Userland/Libraries/LibWeb/CMakeLists.txt @@ -142,6 +142,7 @@ set(SOURCES CSS/StyleValues/PositionStyleValue.cpp CSS/StyleValues/RadialGradientStyleValue.cpp CSS/StyleValues/RectStyleValue.cpp + CSS/StyleValues/RotationStyleValue.cpp CSS/StyleValues/ShadowStyleValue.cpp CSS/StyleValues/ShorthandStyleValue.cpp CSS/StyleValues/StyleValueList.cpp diff --git a/Userland/Libraries/LibWeb/CSS/CSSStyleValue.cpp b/Userland/Libraries/LibWeb/CSS/CSSStyleValue.cpp index 792170111dd..78b5b0a220d 100644 --- a/Userland/Libraries/LibWeb/CSS/CSSStyleValue.cpp +++ b/Userland/Libraries/LibWeb/CSS/CSSStyleValue.cpp @@ -47,6 +47,7 @@ #include #include #include +#include #include #include #include @@ -287,6 +288,12 @@ ResolutionStyleValue const& CSSStyleValue::as_resolution() const return static_cast(*this); } +RotationStyleValue const& CSSStyleValue::as_rotation() const +{ + VERIFY(is_rotation()); + return static_cast(*this); +} + ScrollbarGutterStyleValue const& CSSStyleValue::as_scrollbar_gutter() const { VERIFY(is_scrollbar_gutter()); diff --git a/Userland/Libraries/LibWeb/CSS/CSSStyleValue.h b/Userland/Libraries/LibWeb/CSS/CSSStyleValue.h index 269cb8d2ebf..312b5d6a8bc 100644 --- a/Userland/Libraries/LibWeb/CSS/CSSStyleValue.h +++ b/Userland/Libraries/LibWeb/CSS/CSSStyleValue.h @@ -124,6 +124,7 @@ public: Ratio, Rect, Resolution, + Rotation, ScrollbarGutter, Shadow, Shorthand, @@ -289,6 +290,10 @@ public: ResolutionStyleValue const& as_resolution() const; ResolutionStyleValue& as_resolution() { return const_cast(const_cast(*this).as_resolution()); } + bool is_rotation() const { return type() == Type::Rotation; } + RotationStyleValue const& as_rotation() const; + RotationStyleValue& as_rotation() { return const_cast(const_cast(*this).as_rotation()); } + 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/Userland/Libraries/LibWeb/CSS/ComputedValues.h b/Userland/Libraries/LibWeb/CSS/ComputedValues.h index b48fd60d4e6..ad27ce2c7dd 100644 --- a/Userland/Libraries/LibWeb/CSS/ComputedValues.h +++ b/Userland/Libraries/LibWeb/CSS/ComputedValues.h @@ -494,6 +494,7 @@ public: Vector const& transformations() const { return m_noninherited.transformations; } CSS::TransformBox const& transform_box() const { return m_noninherited.transform_box; } CSS::TransformOrigin const& transform_origin() const { return m_noninherited.transform_origin; } + Optional const& rotate() const { return m_noninherited.rotate; } Gfx::FontCascadeList const& font_list() const { return *m_inherited.font_list; } CSSPixels font_size() const { return m_inherited.font_size; } @@ -664,6 +665,7 @@ protected: CSS::ObjectFit object_fit { InitialValues::object_fit() }; CSS::ObjectPosition object_position { InitialValues::object_position() }; CSS::UnicodeBidi unicode_bidi { InitialValues::unicode_bidi() }; + Optional rotate; Optional mask; CSS::MaskType mask_type { InitialValues::mask_type() }; @@ -775,6 +777,7 @@ public: void set_justify_items(CSS::JustifyItems value) { m_noninherited.justify_items = value; } 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 = 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/Userland/Libraries/LibWeb/CSS/Parser/Parser.cpp b/Userland/Libraries/LibWeb/CSS/Parser/Parser.cpp index 8b83b8b15da..a945d2318df 100644 --- a/Userland/Libraries/LibWeb/CSS/Parser/Parser.cpp +++ b/Userland/Libraries/LibWeb/CSS/Parser/Parser.cpp @@ -77,6 +77,7 @@ #include #include #include +#include #include #include #include @@ -5046,6 +5047,95 @@ RefPtr Parser::parse_single_shadow_value(TokenStream Parser::parse_rotate_value(TokenStream& tokens) +{ + // Value: none | | [ x | y | z | {3} ] && + + if (tokens.remaining_token_count() == 1) { + // "none" + if (auto none = parse_all_as_single_keyword_value(tokens, Keyword::None)) + return none; + + // + if (auto angle = parse_angle_value(tokens)) + return RotationStyleValue::create(angle.release_nonnull(), NumberStyleValue::create(0), NumberStyleValue::create(0), NumberStyleValue::create(1)); + } + + auto parse_one_of_xyz = [&]() -> Optional { + auto transaction = tokens.begin_transaction(); + auto axis = tokens.consume_a_token(); + + if (axis.is_ident("x"sv) || axis.is_ident("y"sv) || axis.is_ident("z"sv)) { + transaction.commit(); + return axis; + } + + return {}; + }; + + // [ x | y | z ] && + if (tokens.remaining_token_count() == 2) { + // Try parsing `x ` + if (auto axis = parse_one_of_xyz(); axis.has_value()) { + if (auto angle = parse_angle_value(tokens); angle) { + if (axis->is_ident("x"sv)) + return RotationStyleValue::create(angle.release_nonnull(), NumberStyleValue::create(1), NumberStyleValue::create(0), NumberStyleValue::create(0)); + if (axis->is_ident("y"sv)) + return RotationStyleValue::create(angle.release_nonnull(), NumberStyleValue::create(0), NumberStyleValue::create(1), NumberStyleValue::create(0)); + if (axis->is_ident("z"sv)) + return RotationStyleValue::create(angle.release_nonnull(), NumberStyleValue::create(0), NumberStyleValue::create(0), NumberStyleValue::create(1)); + } + } + + // Try parsing ` x` + if (auto angle = parse_angle_value(tokens); angle) { + if (auto axis = parse_one_of_xyz(); axis.has_value()) { + if (axis->is_ident("x"sv)) + return RotationStyleValue::create(angle.release_nonnull(), NumberStyleValue::create(1), NumberStyleValue::create(0), NumberStyleValue::create(0)); + if (axis->is_ident("y"sv)) + return RotationStyleValue::create(angle.release_nonnull(), NumberStyleValue::create(0), NumberStyleValue::create(1), NumberStyleValue::create(0)); + if (axis->is_ident("z"sv)) + return RotationStyleValue::create(angle.release_nonnull(), NumberStyleValue::create(0), NumberStyleValue::create(0), NumberStyleValue::create(1)); + } + } + } + + auto parse_three_numbers = [&]() -> Optional { + auto transaction = tokens.begin_transaction(); + StyleValueVector numbers; + for (size_t i = 0; i < 3; ++i) { + if (auto number = parse_number_value(tokens); number) { + numbers.append(number.release_nonnull()); + } else { + return {}; + } + } + transaction.commit(); + return numbers; + }; + + // {3} && + if (tokens.remaining_token_count() == 4) { + // Try parsing {3} + if (auto maybe_numbers = parse_three_numbers(); maybe_numbers.has_value()) { + if (auto angle = parse_angle_value(tokens); angle) { + auto numbers = maybe_numbers.release_value(); + return RotationStyleValue::create(angle.release_nonnull(), numbers[0], numbers[1], numbers[2]); + } + } + + // Try parsing {3} + if (auto angle = parse_angle_value(tokens); angle) { + if (auto maybe_numbers = parse_three_numbers(); maybe_numbers.has_value()) { + auto numbers = maybe_numbers.release_value(); + return RotationStyleValue::create(angle.release_nonnull(), numbers[0], numbers[1], numbers[2]); + } + } + } + + return nullptr; +} + RefPtr Parser::parse_content_value(TokenStream& tokens) { // FIXME: `content` accepts several kinds of function() type, which we don't handle in property_accepts_value() yet. @@ -8205,6 +8295,10 @@ Parser::ParseErrorOr> Parser::parse_css_value(Prope if (auto parsed_value = parse_quotes_value(tokens); parsed_value && !tokens.has_next_token()) return parsed_value.release_nonnull(); return ParseError::SyntaxError; + case PropertyID::Rotate: + if (auto parsed_value = parse_rotate_value(tokens); parsed_value && !tokens.has_next_token()) + return parsed_value.release_nonnull(); + return ParseError::SyntaxError; case PropertyID::ScrollbarGutter: if (auto parsed_value = parse_scrollbar_gutter_value(tokens); parsed_value && !tokens.has_next_token()) return parsed_value.release_nonnull(); diff --git a/Userland/Libraries/LibWeb/CSS/Parser/Parser.h b/Userland/Libraries/LibWeb/CSS/Parser/Parser.h index d0865d55d88..df5a608f3b2 100644 --- a/Userland/Libraries/LibWeb/CSS/Parser/Parser.h +++ b/Userland/Libraries/LibWeb/CSS/Parser/Parser.h @@ -335,6 +335,7 @@ private: RefPtr parse_single_shadow_value(TokenStream&, AllowInsetKeyword); RefPtr parse_text_decoration_value(TokenStream&); RefPtr parse_text_decoration_line_value(TokenStream&); + RefPtr parse_rotate_value(TokenStream&); RefPtr parse_easing_value(TokenStream&); RefPtr parse_transform_value(TokenStream&); RefPtr parse_transform_origin_value(TokenStream&); diff --git a/Userland/Libraries/LibWeb/CSS/Properties.json b/Userland/Libraries/LibWeb/CSS/Properties.json index 42672205c99..6d6bcd4d75f 100644 --- a/Userland/Libraries/LibWeb/CSS/Properties.json +++ b/Userland/Libraries/LibWeb/CSS/Properties.json @@ -2301,6 +2301,13 @@ "unitless-length" ] }, + "rotate": { + "animation-type": "custom", + "inherited": false, + "initial": "none", + "affects-layout": false, + "affects-stacking-context": true + }, "row-gap": { "animation-type": "by-computed-value", "inherited": false, diff --git a/Userland/Libraries/LibWeb/CSS/StyleProperties.cpp b/Userland/Libraries/LibWeb/CSS/StyleProperties.cpp index 5e8b081a564..dfdfe377a77 100644 --- a/Userland/Libraries/LibWeb/CSS/StyleProperties.cpp +++ b/Userland/Libraries/LibWeb/CSS/StyleProperties.cpp @@ -27,6 +27,7 @@ #include #include #include +#include #include #include #include @@ -521,6 +522,43 @@ Vector StyleProperties::transformations() const return transformations_for_style_value(property(CSS::PropertyID::Transform)); } +Optional StyleProperties::rotate(Layout::Node const& layout_node) const +{ + auto value = property(CSS::PropertyID::Rotate); + if (!value->is_rotation()) + return {}; + auto& rotation = value->as_rotation(); + + auto resolve_angle = [&layout_node](CSSStyleValue const& value) -> Optional { + if (value.is_angle()) + return value.as_angle().angle(); + if (value.is_math() && value.as_math().resolves_to_angle()) + return value.as_math().resolve_angle(layout_node); + return {}; + }; + + auto resolve_number = [&](CSSStyleValue const& value) -> Optional { + if (value.is_number()) + return value.as_number().number(); + if (value.is_math() && value.as_math().resolves_to_number()) + return value.as_math().resolve_number(); + return {}; + }; + + auto x = resolve_number(rotation.rotation_x()).value_or(0); + auto y = resolve_number(rotation.rotation_y()).value_or(0); + auto z = resolve_number(rotation.rotation_z()).value_or(0); + auto angle = resolve_angle(rotation.angle()).value_or(Angle::make_degrees(0)); + + Vector values; + values.append({ Number(Number::Type::Number, x) }); + values.append({ Number(Number::Type::Number, y) }); + values.append({ Number(Number::Type::Number, z) }); + values.append({ angle }); + + return CSS::Transformation(CSS::TransformFunction::Rotate3d, move(values)); +} + static Optional length_percentage_for_style_value(CSSStyleValue const& value) { if (value.is_length()) diff --git a/Userland/Libraries/LibWeb/CSS/StyleProperties.h b/Userland/Libraries/LibWeb/CSS/StyleProperties.h index 50cd2eb1776..d7184631464 100644 --- a/Userland/Libraries/LibWeb/CSS/StyleProperties.h +++ b/Userland/Libraries/LibWeb/CSS/StyleProperties.h @@ -173,6 +173,7 @@ public: Vector transformations() const; Optional transform_box() const; CSS::TransformOrigin transform_origin() const; + Optional rotate(Layout::Node const&) const; Optional mask_type() const; Color stop_color() const; diff --git a/Userland/Libraries/LibWeb/CSS/StyleValues/CSSMathValue.cpp b/Userland/Libraries/LibWeb/CSS/StyleValues/CSSMathValue.cpp index 7283e3397d2..f815a6be32a 100644 --- a/Userland/Libraries/LibWeb/CSS/StyleValues/CSSMathValue.cpp +++ b/Userland/Libraries/LibWeb/CSS/StyleValues/CSSMathValue.cpp @@ -2650,6 +2650,20 @@ Optional CSSMathValue::resolve_angle() const return {}; } +Optional CSSMathValue::resolve_angle(Layout::Node const& layout_node) const +{ + return resolve_angle(Length::ResolutionContext::for_layout_node(layout_node)); +} + +Optional CSSMathValue::resolve_angle(Length::ResolutionContext const& context) const +{ + auto result = m_calculation->resolve(context, {}); + + if (result.value().has()) + return result.value().get(); + return {}; +} + Optional CSSMathValue::resolve_angle_percentage(Angle const& percentage_basis) const { auto result = m_calculation->resolve({}, percentage_basis); diff --git a/Userland/Libraries/LibWeb/CSS/StyleValues/CSSMathValue.h b/Userland/Libraries/LibWeb/CSS/StyleValues/CSSMathValue.h index bd999221d94..3c8ae4791aa 100644 --- a/Userland/Libraries/LibWeb/CSS/StyleValues/CSSMathValue.h +++ b/Userland/Libraries/LibWeb/CSS/StyleValues/CSSMathValue.h @@ -85,6 +85,8 @@ public: bool resolves_to_angle() const { return m_resolved_type.matches_angle(); } bool resolves_to_angle_percentage() const { return m_resolved_type.matches_angle_percentage(); } Optional resolve_angle() const; + Optional resolve_angle(Layout::Node const& layout_node) const; + Optional resolve_angle(Length::ResolutionContext const& context) const; Optional resolve_angle_percentage(Angle const& percentage_basis) const; bool resolves_to_flex() const { return m_resolved_type.matches_flex(); } diff --git a/Userland/Libraries/LibWeb/CSS/StyleValues/RotationStyleValue.cpp b/Userland/Libraries/LibWeb/CSS/StyleValues/RotationStyleValue.cpp new file mode 100644 index 00000000000..fa1008d6ee7 --- /dev/null +++ b/Userland/Libraries/LibWeb/CSS/StyleValues/RotationStyleValue.cpp @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2024, Steffen T. Larssen + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include +#include +#include + +#include "RotationStyleValue.h" + +namespace Web::CSS { + +// https://www.w3.org/TR/2021/WD-css-transforms-2-20211109/#individual-transform-serialization +String RotationStyleValue::to_string() const +{ + auto resolve_to_number = [](ValueComparingNonnullRefPtr const& value) -> Optional { + if (value->is_number()) + return value->as_number().number(); + if (value->is_math() && value->as_math().resolves_to_number()) + return value->as_math().resolve_number(); + + VERIFY_NOT_REACHED(); + }; + + auto x_value = resolve_to_number(m_properties.rotation_x).value_or(0); + auto y_value = resolve_to_number(m_properties.rotation_y).value_or(0); + auto z_value = resolve_to_number(m_properties.rotation_z).value_or(0); + + // If the axis is parallel with the x or y axes, it must serialize as the appropriate keyword. + if (x_value > 0.0 && y_value == 0 && z_value == 0) + return MUST(String::formatted("x {}", m_properties.angle->to_string())); + + if (x_value == 0 && y_value > 0.0 && z_value == 0) + return MUST(String::formatted("y {}", m_properties.angle->to_string())); + + // If a rotation about the z axis (that is, in 2D) is specified, the property must serialize as just an . + if (x_value == 0 && y_value == 0 && z_value > 0.0) + return m_properties.angle->to_string(); + + // It must serialize as the keyword none if and only if none was originally specified. + // NOTE: This is handled by returning a keyword from the parser. + + // If any other rotation is specified, the property must serialize with an axis specified. + return MUST(String::formatted("{} {} {} {}", m_properties.rotation_x->to_string(), m_properties.rotation_y->to_string(), m_properties.rotation_z->to_string(), m_properties.angle->to_string())); +} + +} diff --git a/Userland/Libraries/LibWeb/CSS/StyleValues/RotationStyleValue.h b/Userland/Libraries/LibWeb/CSS/StyleValues/RotationStyleValue.h new file mode 100644 index 00000000000..53554c54216 --- /dev/null +++ b/Userland/Libraries/LibWeb/CSS/StyleValues/RotationStyleValue.h @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2024, Steffen T. Larssen + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include + +namespace Web::CSS { + +class RotationStyleValue : public StyleValueWithDefaultOperators { +public: + static ValueComparingNonnullRefPtr create(ValueComparingNonnullRefPtr angle, ValueComparingNonnullRefPtr rotation_x, ValueComparingNonnullRefPtr rotation_y, ValueComparingNonnullRefPtr rotation_z) + { + return adopt_ref(*new (nothrow) RotationStyleValue(move(angle), move(rotation_x), move(rotation_y), move(rotation_z))); + } + + virtual ~RotationStyleValue() override = default; + + ValueComparingNonnullRefPtr const& angle() const { return m_properties.angle; } + ValueComparingNonnullRefPtr const& rotation_x() const { return m_properties.rotation_x; } + ValueComparingNonnullRefPtr const& rotation_y() const { return m_properties.rotation_y; } + ValueComparingNonnullRefPtr const& rotation_z() const { return m_properties.rotation_z; } + + virtual String to_string() const override; + + bool properties_equal(RotationStyleValue const& other) const { return m_properties == other.m_properties; } + +private: + explicit RotationStyleValue( + ValueComparingNonnullRefPtr angle, + ValueComparingNonnullRefPtr rotation_x, + ValueComparingNonnullRefPtr rotation_y, + ValueComparingNonnullRefPtr rotation_z) + : StyleValueWithDefaultOperators(Type::Rotation) + , m_properties { + .angle = move(angle), + .rotation_x = move(rotation_x), + .rotation_y = move(rotation_y), + .rotation_z = move(rotation_z) + } + { + } + + struct Properties { + ValueComparingNonnullRefPtr angle; + ValueComparingNonnullRefPtr rotation_x; + ValueComparingNonnullRefPtr rotation_y; + ValueComparingNonnullRefPtr rotation_z; + bool operator==(Properties const&) const = default; + } m_properties; +}; + +} diff --git a/Userland/Libraries/LibWeb/Forward.h b/Userland/Libraries/LibWeb/Forward.h index 4c324bfbdb3..96ed5443da3 100644 --- a/Userland/Libraries/LibWeb/Forward.h +++ b/Userland/Libraries/LibWeb/Forward.h @@ -199,6 +199,7 @@ class RectStyleValue; class Resolution; class ResolutionOrCalculated; class ResolutionStyleValue; +class RotationStyleValue; class Screen; class ScreenOrientation; class ScrollbarGutterStyleValue; diff --git a/Userland/Libraries/LibWeb/Layout/Node.cpp b/Userland/Libraries/LibWeb/Layout/Node.cpp index d0ce10c9124..5af49f68478 100644 --- a/Userland/Libraries/LibWeb/Layout/Node.cpp +++ b/Userland/Libraries/LibWeb/Layout/Node.cpp @@ -708,6 +708,9 @@ void NodeWithStyle::apply_style(const CSS::StyleProperties& computed_style) computed_values.set_box_shadow(computed_style.box_shadow(*this)); + if (auto rotate_value = computed_style.rotate(*this); rotate_value.has_value()) + computed_values.set_rotate(rotate_value.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());