diff --git a/Libraries/LibWeb/CSS/Keywords.json b/Libraries/LibWeb/CSS/Keywords.json index 16e6501784..d7ad38e0b8 100644 --- a/Libraries/LibWeb/CSS/Keywords.json +++ b/Libraries/LibWeb/CSS/Keywords.json @@ -443,6 +443,7 @@ "new-base-60", "newa", "nko-cardinal", + "no-clamp", "no-clip", "no-close-quote", "no-common-ligatures", diff --git a/Libraries/LibWeb/CSS/MathFunctions.json b/Libraries/LibWeb/CSS/MathFunctions.json index cd4c4ea138..3ae4067abd 100644 --- a/Libraries/LibWeb/CSS/MathFunctions.json +++ b/Libraries/LibWeb/CSS/MathFunctions.json @@ -168,6 +168,27 @@ } ] }, + "progress": { + "parameter-validation": "consistent", + "__comment": "`no-clamp` is implemented ad-hoc in the parser since it's unusual in that it isn't comma separated", + "parameters": [ + { + "name": "value", + "type": "||", + "required": true + }, + { + "name": "start-value", + "type": "||", + "required": true + }, + { + "name": "end-value", + "type": "||", + "required": true + } + ] + }, "random": { "parameter-validation": "consistent", "parameters": [ diff --git a/Libraries/LibWeb/CSS/StyleValues/CalculatedStyleValue.cpp b/Libraries/LibWeb/CSS/StyleValues/CalculatedStyleValue.cpp index 6f73183be3..0327f7e37b 100644 --- a/Libraries/LibWeb/CSS/StyleValues/CalculatedStyleValue.cpp +++ b/Libraries/LibWeb/CSS/StyleValues/CalculatedStyleValue.cpp @@ -320,7 +320,7 @@ static void serialize_a_math_function(StringBuilder& builder, CalculationNode co // Concatenate all of the results using ", " (comma followed by space), then append the result to s. // Spec issue: https://github.com/w3c/csswg-drafts/issues/11783 - // The three AD-HOCs in this step are mentioned there. + // The four AD-HOCs in this step are mentioned there. // AD-HOC: Numeric nodes have no children and should serialize directly. // AD-HOC: calc-operator nodes should also serialize directly, instead of separating their children by commas.# if (fn.type() == CalculationNode::Type::Numeric || fn.is_calc_operator_node()) { @@ -334,6 +334,14 @@ static void serialize_a_math_function(StringBuilder& builder, CalculationNode co builder.append(CSS::to_string(rounding_strategy)); first = false; } + + // AD-HOC: For `progress()` we serialize 'no-clamp' separately since it's not a calculation node and shouldn't + // be followed by a comma. + if (fn.type() == CalculationNode::Type::Progress) { + if (static_cast(fn).no_clamp()) + builder.append("no-clamp "sv); + } + for (auto const& child : fn.children()) { if (!first) builder.append(", "sv); @@ -612,6 +620,8 @@ StringView CalculationNode::name() const return "atan2"sv; case Type::Pow: return "pow"sv; + case Type::Progress: + return "progress"sv; case Type::Sqrt: return "sqrt"sv; case Type::Hypot: @@ -1004,6 +1014,113 @@ GC::Ptr ProductCalculationNode::reify(JS::Realm& realm) const return CSSMathProduct::create(realm, numeric_type().value(), reified_children.as_nonnull()); } +NonnullRefPtr ProgressCalculationNode::create(bool no_clamp, NonnullRefPtr value, NonnullRefPtr start_value, NonnullRefPtr end_value) +{ + // https://drafts.csswg.org/css-values-5/#progress + // The result of progress() is a made consistent with the consistent type of its arguments + auto numeric_type = NumericType {}.made_consistent_with(add_the_types({ value, start_value, end_value }).value()).value(); + + return adopt_ref(*new (nothrow) ProgressCalculationNode(no_clamp, move(value), move(start_value), move(end_value), numeric_type)); +} + +ProgressCalculationNode::ProgressCalculationNode(bool no_clamp, NonnullRefPtr value, NonnullRefPtr start_value, NonnullRefPtr end_value, Optional numeric_type) + : CalculationNode(Type::Progress, numeric_type) + , m_no_clamp(no_clamp) + , m_value(move(value)) + , m_start_value(move(start_value)) + , m_end_value(move(end_value)) +{ +} + +ProgressCalculationNode::~ProgressCalculationNode() = default; + +bool ProgressCalculationNode::contains_percentage() const +{ + return m_value->contains_percentage() + || m_start_value->contains_percentage() + || m_end_value->contains_percentage(); +} + +NonnullRefPtr ProgressCalculationNode::with_simplified_children(CalculationContext const& calculation_context, CalculationResolutionContext const& calculation_resolution_context) const +{ + auto simplified_value = simplify_a_calculation_tree(m_value, calculation_context, calculation_resolution_context); + auto simplified_start_value = simplify_a_calculation_tree(m_start_value, calculation_context, calculation_resolution_context); + auto simplified_end_value = simplify_a_calculation_tree(m_end_value, calculation_context, calculation_resolution_context); + + if (simplified_value == m_value && simplified_start_value == m_start_value && simplified_end_value == m_end_value) + return *this; + + return create(m_no_clamp, move(simplified_value), move(simplified_start_value), move(simplified_end_value)); +} + +Optional ProgressCalculationNode::run_operation_if_possible(CalculationContext const& context, CalculationResolutionContext const& resolution_context) const +{ + auto maybe_value = try_get_value_with_canonical_unit(*m_value, context, resolution_context); + auto maybe_start_value = try_get_value_with_canonical_unit(*m_start_value, context, resolution_context); + auto maybe_end_value = try_get_value_with_canonical_unit(*m_end_value, context, resolution_context); + + if (!maybe_value.has_value() || !maybe_start_value.has_value() || !maybe_end_value.has_value()) + return {}; + + // https://drafts.csswg.org/css-values-5/#calculate-a-progress-function + // If the progress start value and progress end value are different values + if (maybe_start_value != maybe_end_value) { + // (progress value - progress start value) / (progress end value - progress start value), clamped to the [0,1] range if no-clamp is not specified. + auto progress = (maybe_value->value() - maybe_start_value->value()) / (maybe_end_value->value() - maybe_start_value->value()); + + if (!m_no_clamp) + progress = clamp(progress, 0.0, 1.0); + + return CalculatedStyleValue::CalculationResult { progress, numeric_type() }; + } + + // If the progress start value and progress end value are the same value + { + // 0 if no-clamp is not specified. + if (!m_no_clamp) + return CalculatedStyleValue::CalculationResult { 0.0, numeric_type() }; + + // Otherwise, 0, -∞, or +∞, depending on whether progress value is equal to, less than, or greater than the shared value. + if (maybe_value->value() == maybe_start_value->value()) + return CalculatedStyleValue::CalculationResult { 0.0, numeric_type() }; + if (maybe_value->value() < maybe_start_value->value()) + return CalculatedStyleValue::CalculationResult { -AK::Infinity, numeric_type() }; + + return CalculatedStyleValue::CalculationResult { AK::Infinity, numeric_type() }; + } +} + +void ProgressCalculationNode::dump(StringBuilder& builder, int indent) const +{ + builder.appendff("{: >{}}PROGRESS (no-clamp: {})\n", "", indent, m_no_clamp); + m_value->dump(builder, indent + 2); + m_start_value->dump(builder, indent + 2); + m_end_value->dump(builder, indent + 2); +} + +bool ProgressCalculationNode::equals(CalculationNode const& other) const +{ + if (this == &other) + return true; + + if (type() != other.type()) + return false; + + auto const& other_progress = static_cast(other); + + return m_no_clamp == other_progress.m_no_clamp + && m_value->equals(other_progress.m_value) + && m_start_value->equals(other_progress.m_start_value) + && m_end_value->equals(other_progress.m_end_value); +} + +bool ProgressCalculationNode::is_computationally_independent() const +{ + return m_value->is_computationally_independent() + && m_start_value->is_computationally_independent() + && m_end_value->is_computationally_independent(); +} + NonnullRefPtr NegateCalculationNode::create(NonnullRefPtr value) { return adopt_ref(*new (nothrow) NegateCalculationNode(move(value))); diff --git a/Libraries/LibWeb/CSS/StyleValues/CalculatedStyleValue.h b/Libraries/LibWeb/CSS/StyleValues/CalculatedStyleValue.h index d842dc506c..3b77a3ab07 100644 --- a/Libraries/LibWeb/CSS/StyleValues/CalculatedStyleValue.h +++ b/Libraries/LibWeb/CSS/StyleValues/CalculatedStyleValue.h @@ -150,6 +150,7 @@ private: X(Clamp) \ X(Sum) \ X(Product) \ + X(Progress) \ X(Negate) \ X(Invert) \ X(Abs) \ @@ -217,6 +218,7 @@ public: case Type::Atan: case Type::Atan2: case Type::Pow: + case Type::Progress: case Type::Sqrt: case Type::Hypot: case Type::Log: @@ -335,6 +337,32 @@ private: Vector> m_values; }; +class ProgressCalculationNode final : public CalculationNode { +public: + static NonnullRefPtr create(bool no_clamp, NonnullRefPtr value, NonnullRefPtr start_value, NonnullRefPtr end_value); + ~ProgressCalculationNode(); + + virtual bool contains_percentage() const override; + virtual NonnullRefPtr with_simplified_children(CalculationContext const&, CalculationResolutionContext const&) const override; + virtual Optional run_operation_if_possible(CalculationContext const&, CalculationResolutionContext const&) const override; + + virtual Vector> children() const override { return { { m_value, m_start_value, m_end_value } }; } + + virtual void dump(StringBuilder&, int indent) const override; + virtual bool equals(CalculationNode const&) const override; + virtual bool is_computationally_independent() const override; + + bool no_clamp() const { return m_no_clamp; } + +private: + ProgressCalculationNode(bool no_clamp, NonnullRefPtr value, NonnullRefPtr start_value, NonnullRefPtr end_value, Optional numeric_type); + + bool m_no_clamp; + NonnullRefPtr m_value; + NonnullRefPtr m_start_value; + NonnullRefPtr m_end_value; +}; + class NegateCalculationNode final : public CalculationNode { public: static NonnullRefPtr create(NonnullRefPtr); diff --git a/Meta/Generators/generate_libweb_css_math_functions.py b/Meta/Generators/generate_libweb_css_math_functions.py index 470257b74a..ec0f2e5986 100644 --- a/Meta/Generators/generate_libweb_css_math_functions.py +++ b/Meta/Generators/generate_libweb_css_math_functions.py @@ -124,6 +124,15 @@ static Optional parse_rounding_strategy(TokenStream Parser::parse_math_function(Function const& function, CalculationContext const& context) { TokenStream stream { function.value }; + + // AD-HOC: `progress()`'s `no-clamp` argument that isn't comma separated like the other arguments, so we need to + // parse it first. + bool progress_function_no_clamp = false; + if (function.name.equals_ignoring_ascii_case("progress"sv)) { + if (auto keyword = parse_specific_keyword_value(stream, { { Keyword::NoClamp } })) + progress_function_no_clamp = true; + } + auto arguments = parse_a_comma_separated_list_of_component_values(stream); auto const& percentages_resolve_as = context.percentages_resolve_as; """) @@ -393,6 +402,11 @@ RefPtr Parser::parse_math_function(Function const& functi """) # Generate the call to the constructor out.write(f" return {name_titlecase}CalculationNode::create(") + if name == "progress": + out.write("progress_function_no_clamp") + if parameters: + out.write(", ") + for parameter_index, parameter in enumerate(parameters): parameter_type_string = parameter["type"] if parameter_type_string == "": diff --git a/Tests/LibWeb/Text/expected/css/progress-no-clamp.txt b/Tests/LibWeb/Text/expected/css/progress-no-clamp.txt new file mode 100644 index 0000000000..c2197a7453 --- /dev/null +++ b/Tests/LibWeb/Text/expected/css/progress-no-clamp.txt @@ -0,0 +1,12 @@ +specified: calc(-1) +computed: -1 +specified: calc(2) +computed: 2 +specified: progress(no-clamp 1em, 0px, 100px) +computed: 0 +specified: calc(-infinity) +computed: -2147483648 +specified: calc(0) +computed: 0 +specified: calc(infinity) +computed: 2147483647 diff --git a/Tests/LibWeb/Text/expected/wpt-import/css/css-values/progress-computed.txt b/Tests/LibWeb/Text/expected/wpt-import/css/css-values/progress-computed.txt new file mode 100644 index 0000000000..dcb56a07f0 --- /dev/null +++ b/Tests/LibWeb/Text/expected/wpt-import/css/css-values/progress-computed.txt @@ -0,0 +1,28 @@ +Harness status: OK + +Found 23 tests + +23 Pass +Pass progress(1, 0, 1) should be used-value-equivalent to 1 +Pass progress(progress(1, 0, 1), progress(0px, 0px, 1px), progress(1deg, 0deg, 1deg)) should be used-value-equivalent to 1 +Pass progress(sign(-10px) * 10px, (10px - 10px), 10px * progress(1deg, 0deg, 1deg)) should be used-value-equivalent to 0 +Pass calc(progress(100px, 0px, 50px) * 10px + 100px) should be used-value-equivalent to 110px +Pass calc(progress(100, 0, sign(50px))) should be used-value-equivalent to 1 +Pass calc(progress(abs(5%), hypot(3%, 4%), 10%)) should be used-value-equivalent to 0 +Pass progress(50em, 10em, 110em) should be used-value-equivalent to 0.4 +Pass progress(1000em, 10em, 110em) should be used-value-equivalent to 1 +Pass scale(progress(50em, 10rem, 110em)) should be used-value-equivalent to scale(1) +Pass scale(progress(1000em, 10rem, 110em)) should be used-value-equivalent to scale(1) +Pass scale(progress(0em, 0rem, 0em)) should be used-value-equivalent to scale(0) +Pass scale(progress(sign(1em - 1rem) * 1ex, 0rem, 0em)) should be used-value-equivalent to scale(0) +Pass calc(progress(1, 0, 1) * 10px) should be used-value-equivalent to 10px +Pass calc(progress(1, 0, 1) * 1s) should be used-value-equivalent to 1s +Pass calc(progress(1, 0, 1) * 1deg) should be used-value-equivalent to 1deg +Pass calc(progress(sign(1001em - 10lh * progress(100px, 2rex, 10ex)) * 10em, 2rem, 12em) / 2) should be used-value-equivalent to 0.4 +Pass calc(progress(sign(1001em - 10lh * progress(100px, 2rex, 10ex)) * 20em, 2rem, 12em) * 10) should be used-value-equivalent to 10 +Pass calc(progress(sign(1001em - 10lh * progress(100px, 2rex, 10ex)) * 20em, 2rem, 12em) * 30) should be used-value-equivalent to 30 +Pass calc(progress(sign(1001em - 10lh * progress(100px, 2rex, 10ex)) * 20em, 2rem, 12em) / 4) should be used-value-equivalent to 0.25 +Pass calc(progress(sign(1001em - 10lh * progress(100px, 2rex, 10ex)) * 20em, 2rem, 12em) * 4) should be used-value-equivalent to 4 +Pass calc(progress(sign(1001em - 10lh * progress(100px, 2rex, 10ex)) * 20em, 2rem, 12em) * 2) should be used-value-equivalent to 2 +Pass rotate3d(progress(21em, 1rem, 11em), progress(21em, 1rem, 11em), progress(21em, 1rem, 11em), calc(progress(11em, 1rem, 11em) * 2deg)) should be used-value-equivalent to rotate3d(2, 2, 2, 2deg) +Pass calc(50% - 0px * clamp(0, progress(0cqw, 0px, 0px), 1)) should be used-value-equivalent to 50px \ No newline at end of file diff --git a/Tests/LibWeb/Text/expected/wpt-import/css/css-values/progress-invalid.txt b/Tests/LibWeb/Text/expected/wpt-import/css/css-values/progress-invalid.txt new file mode 100644 index 0000000000..4234dee8b7 --- /dev/null +++ b/Tests/LibWeb/Text/expected/wpt-import/css/css-values/progress-invalid.txt @@ -0,0 +1,32 @@ +Harness status: OK + +Found 27 tests + +27 Pass +Pass e.style['opacity'] = "progress()" should not set the property value +Pass e.style['opacity'] = "progress( )" should not set the property value +Pass e.style['opacity'] = "progress(,)" should not set the property value +Pass e.style['opacity'] = "progress(1, )" should not set the property value +Pass e.style['opacity'] = "progress(1)" should not set the property value +Pass e.style['opacity'] = "progress(50%, 0)" should not set the property value +Pass e.style['opacity'] = "progress(0, 1,)" should not set the property value +Pass e.style['opacity'] = "progress(from,)" should not set the property value +Pass e.style['opacity'] = "progress(from 1, 0)" should not set the property value +Pass e.style['opacity'] = "progress(from 1, 0 1)" should not set the property value +Pass e.style['opacity'] = "progress(from 1 0)" should not set the property value +Pass e.style['opacity'] = "progress(0,, 0)" should not set the property value +Pass e.style['opacity'] = "progress(to,,,,)" should not set the property value +Pass e.style['opacity'] = "progress(0,,, 10,, 200)" should not set the property value +Pass e.style['opacity'] = "progress(0,,, 10,,, 200)" should not set the property value +Pass e.style['opacity'] = "progress(0,, 10,, 200)" should not set the property value +Pass e.style['opacity'] = "progress(0 from 10 to 200)" should not set the property value +Pass e.style['opacity'] = "progress(5%, 0, 8" should not set the property value +Pass e.style['opacity'] = "progress(5%, 0deg, 8deg" should not set the property value +Pass e.style['opacity'] = "progress(5, 0deg, 8deg" should not set the property value +Pass e.style['opacity'] = "progress(5, 0%, 8deg" should not set the property value +Pass e.style['opacity'] = "progress(5, 0%, sign(10px)" should not set the property value +Pass e.style['opacity'] = "progress(5%, 0px, 10px" should not set the property value +Pass e.style['letter-spacing'] = "calc(1px * progress(10deg, 0, 10))" should not set the property value +Pass e.style['letter-spacing'] = "calc(1px * progress(10, 0px, 10))" should not set the property value +Pass e.style['opacity'] = "progress(10px * 10px, 10px * 10px, 10px * 10px)" should not set the property value +Pass e.style['opacity'] = "progress(10px, 10px * 10px, 10px * 10px)" should not set the property value \ No newline at end of file diff --git a/Tests/LibWeb/Text/expected/wpt-import/css/css-values/progress-serialize.txt b/Tests/LibWeb/Text/expected/wpt-import/css/css-values/progress-serialize.txt new file mode 100644 index 0000000000..33935db2fe --- /dev/null +++ b/Tests/LibWeb/Text/expected/wpt-import/css/css-values/progress-serialize.txt @@ -0,0 +1,39 @@ +Harness status: OK + +Found 34 tests + +34 Pass +Pass 'progress(100px, 0px, 100px)' as a specified value should serialize as 'calc(1)'. +Pass 'scale(progress(100px, 0px, 100px))' as a specified value should serialize as 'scale(calc(1))'. +Pass 'progress(100px, 0px, 100px)' as a computed value should serialize as '1'. +Pass 'scale(progress(100px, 0px, 100px))' as a computed value should serialize as 'matrix(1, 0, 0, 1, 0, 0)'. +Pass 'progress(10em, 0px, 10em)' as a specified value should serialize as 'progress(10em, 0px, 10em)'. +Pass 'scale(progress(10em, 0px, 10em))' as a specified value should serialize as 'scale(progress(10em, 0px, 10em))'. +Pass 'progress(10em, 0px, 10em)' as a computed value should serialize as '1'. +Pass 'scale(progress(10em, 0px, 10em))' as a computed value should serialize as 'matrix(1, 0, 0, 1, 0, 0)'. +Pass 'progress(10em, 0px, 10rem)' as a specified value should serialize as 'progress(10em, 0px, 10rem)'. +Pass 'scale(progress(10em, 0px, 10rem))' as a specified value should serialize as 'scale(progress(10em, 0px, 10rem))'. +Pass 'progress(10em, 0px, 10rem)' as a computed value should serialize as '1'. +Pass 'scale(progress(10em, 0px, 10rem))' as a computed value should serialize as 'matrix(1, 0, 0, 1, 0, 0)'. +Pass 'progress(100px, (10px - 10px), 100px)' as a specified value should serialize as 'calc(1)'. +Pass 'scale(progress(100px, (10px - 10px), 100px))' as a specified value should serialize as 'scale(calc(1))'. +Pass 'progress(100px, (10px - 10px), 100px)' as a computed value should serialize as '1'. +Pass 'scale(progress(100px, (10px - 10px), 100px))' as a computed value should serialize as 'matrix(1, 0, 0, 1, 0, 0)'. +Pass 'progress(1%, (10% - 10%), 100%)' as a specified value should serialize as 'calc(0.01)'. +Pass 'scale(progress(1%, (10% - 10%), 100%))' as a specified value should serialize as 'scale(calc(0.01))'. +Pass 'progress(1%, (10% - 10%), 100%)' as a computed value should serialize as '0.01'. +Pass 'scale(progress(1%, (10% - 10%), 100%))' as a computed value should serialize as 'matrix(0.01, 0, 0, 0.01, 0, 0)'. +Pass 'calc(0.5 * progress(100px, 0px, 100px))' as a specified value should serialize as 'calc(0.5)'. +Pass 'scale(calc(0.5 * progress(100px, 0px, 100px)))' as a specified value should serialize as 'scale(calc(0.5))'. +Pass 'calc(0.5 * progress(100px, 0px, 100px))' as a computed value should serialize as '0.5'. +Pass 'scale(calc(0.5 * progress(100px, 0px, 100px)))' as a computed value should serialize as 'matrix(0.5, 0, 0, 0.5, 0, 0)'. +Pass 'calc(0.5 * progress(200px, 0px, 100px))' as a specified value should serialize as 'calc(0.5)'. +Pass 'scale(calc(0.5 * progress(200px, 0px, 100px)))' as a specified value should serialize as 'scale(calc(0.5))'. +Pass 'calc(0.5 * progress(200px, 0px, 100px))' as a computed value should serialize as '0.5'. +Pass 'scale(calc(0.5 * progress(200px, 0px, 100px)))' as a computed value should serialize as 'matrix(0.5, 0, 0, 0.5, 0, 0)'. +Pass 'calc(0.5 * progress(-100px, 0px, 100px))' as a specified value should serialize as 'calc(0)'. +Pass 'scale(calc(0.5 * progress(-100px, 0px, 100px)))' as a specified value should serialize as 'scale(calc(0))'. +Pass 'calc(0.5 * progress(-100px, 0px, 100px))' as a computed value should serialize as '0'. +Pass 'scale(calc(0.5 * progress(-100px, 0px, 100px)))' as a computed value should serialize as 'matrix(0, 0, 0, 0, 0, 0)'. +Pass 'calc(50px * progress(100px, 0px, 100px))' as a specified value should serialize as 'calc(50px)'. +Pass 'calc(1px * progress(abs(10%), (10% - 10%), 100% / 10))' as a computed value should serialize as '1px'. \ No newline at end of file diff --git a/Tests/LibWeb/Text/input/css/progress-no-clamp.html b/Tests/LibWeb/Text/input/css/progress-no-clamp.html new file mode 100644 index 0000000000..4a979b1879 --- /dev/null +++ b/Tests/LibWeb/Text/input/css/progress-no-clamp.html @@ -0,0 +1,28 @@ + + + diff --git a/Tests/LibWeb/Text/input/wpt-import/css/css-values/progress-computed.html b/Tests/LibWeb/Text/input/wpt-import/css/css-values/progress-computed.html new file mode 100644 index 0000000000..0f0a496593 --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/css/css-values/progress-computed.html @@ -0,0 +1,56 @@ + + + + + + + +
+
+
+ diff --git a/Tests/LibWeb/Text/input/wpt-import/css/css-values/progress-invalid.html b/Tests/LibWeb/Text/input/wpt-import/css/css-values/progress-invalid.html new file mode 100644 index 0000000000..1430e6809a --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/css/css-values/progress-invalid.html @@ -0,0 +1,46 @@ + + + + + + + diff --git a/Tests/LibWeb/Text/input/wpt-import/css/css-values/progress-serialize.html b/Tests/LibWeb/Text/input/wpt-import/css/css-values/progress-serialize.html new file mode 100644 index 0000000000..7be348a891 --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/css/css-values/progress-serialize.html @@ -0,0 +1,66 @@ + + + + + + +
+