diff --git a/Libraries/LibWeb/CSS/StyleValues/CalculatedStyleValue.cpp b/Libraries/LibWeb/CSS/StyleValues/CalculatedStyleValue.cpp index 82ebab4b9aa..aa2cb225509 100644 --- a/Libraries/LibWeb/CSS/StyleValues/CalculatedStyleValue.cpp +++ b/Libraries/LibWeb/CSS/StyleValues/CalculatedStyleValue.cpp @@ -8,6 +8,7 @@ */ #include "CalculatedStyleValue.h" +#include #include #include @@ -81,6 +82,44 @@ static Optional multiply_the_types(Vector +static NonnullRefPtr simplify_children_vector(T const& original, CalculationContext const& context, CalculationResolutionContext const& resolution_context) +{ + Vector> simplified_children; + simplified_children.ensure_capacity(original.children().size()); + + bool any_changed = false; + for (auto const& child : original.children()) { + auto simplified = simplify_a_calculation_tree(child, context, resolution_context); + if (simplified != child) + any_changed = true; + simplified_children.append(move(simplified)); + } + + if (any_changed) + return T::create(move(simplified_children)); + return original; +} + +template +static NonnullRefPtr simplify_child(T const& original, NonnullRefPtr const& child, CalculationContext const& context, CalculationResolutionContext const& resolution_context) +{ + auto simplified = simplify_a_calculation_tree(child, context, resolution_context); + if (simplified != child) + return T::create(move(simplified)); + return original; +} + +template +static NonnullRefPtr simplify_2_children(T const& original, NonnullRefPtr const& child_1, NonnullRefPtr const& child_2, CalculationContext const& context, CalculationResolutionContext const& resolution_context) +{ + auto simplified_1 = simplify_a_calculation_tree(child_1, context, resolution_context); + auto simplified_2 = simplify_a_calculation_tree(child_2, context, resolution_context); + if (simplified_1 != child_1 || simplified_2 != child_2) + return T::create(move(simplified_1), move(simplified_2)); + return original; +} + Optional CalculationNode::constant_type_from_string(StringView string) { if (string.equals_ignoring_ascii_case("e"sv)) @@ -198,6 +237,48 @@ bool NumericCalculationNode::contains_percentage() const return m_value.has(); } +bool NumericCalculationNode::is_in_canonical_unit() const +{ + return m_value.visit( + [](Angle const& angle) { return angle.type() == Angle::Type::Deg; }, + [](Flex const& flex) { return flex.type() == Flex::Type::Fr; }, + [](Frequency const& frequency) { return frequency.type() == Frequency::Type::Hz; }, + [](Length const& length) { return length.type() == Length::Type::Px; }, + [](Number const&) { return true; }, + [](Percentage const&) { return true; }, + [](Resolution const& resolution) { return resolution.type() == Resolution::Type::Dppx; }, + [](Time const& time) { return time.type() == Time::Type::S; }); +} + +static Optional try_get_value_with_canonical_unit(CalculationNode const& child, CalculationContext const& context, CalculationResolutionContext const& resolution_context) +{ + if (child.type() != CalculationNode::Type::Numeric) + return {}; + auto const& numeric_child = as(child); + + // Can't run with non-canonical units or unresolved percentages. + // We've already attempted to resolve both in with_simplified_children(). + if (!numeric_child.is_in_canonical_unit() + || (numeric_child.value().has() && context.percentages_resolve_as.has_value())) + return {}; + + // Can't run if a child has an invalid type. + if (!numeric_child.numeric_type().has_value()) + return {}; + + return CalculatedStyleValue::CalculationResult::from_value(numeric_child.value(), resolution_context, numeric_child.numeric_type()); +} + +static Optional try_get_number(CalculationNode const& child) +{ + if (child.type() != CalculationNode::Type::Numeric) + return {}; + auto const* maybe_number = as(child).value().get_pointer(); + if (!maybe_number) + return {}; + return maybe_number->value(); +} + CalculatedStyleValue::CalculationResult NumericCalculationNode::resolve(CalculationResolutionContext const& context) const { if (m_value.has()) { @@ -288,6 +369,11 @@ CalculatedStyleValue::CalculationResult SumCalculationNode::resolve(CalculationR return total.value(); } +NonnullRefPtr SumCalculationNode::with_simplified_children(CalculationContext const& context, CalculationResolutionContext const& resolution_context) const +{ + return simplify_children_vector(*this, context, resolution_context); +} + void SumCalculationNode::dump(StringBuilder& builder, int indent) const { builder.appendff("{: >{}}SUM:\n", "", indent); @@ -366,6 +452,11 @@ CalculatedStyleValue::CalculationResult ProductCalculationNode::resolve(Calculat return total.value(); } +NonnullRefPtr ProductCalculationNode::with_simplified_children(CalculationContext const& context, CalculationResolutionContext const& resolution_context) const +{ + return simplify_children_vector(*this, context, resolution_context); +} + void ProductCalculationNode::dump(StringBuilder& builder, int indent) const { builder.appendff("{: >{}}PRODUCT:\n", "", indent); @@ -419,6 +510,11 @@ CalculatedStyleValue::CalculationResult NegateCalculationNode::resolve(Calculati return child_value; } +NonnullRefPtr NegateCalculationNode::with_simplified_children(CalculationContext const& context, CalculationResolutionContext const& resolution_context) const +{ + return simplify_child(*this, m_value, context, resolution_context); +} + void NegateCalculationNode::dump(StringBuilder& builder, int indent) const { builder.appendff("{: >{}}NEGATE:\n", "", indent); @@ -471,6 +567,11 @@ CalculatedStyleValue::CalculationResult InvertCalculationNode::resolve(Calculati return child_value; } +NonnullRefPtr InvertCalculationNode::with_simplified_children(CalculationContext const& context, CalculationResolutionContext const& resolution_context) const +{ + return simplify_child(*this, m_value, context, resolution_context); +} + void InvertCalculationNode::dump(StringBuilder& builder, int indent) const { builder.appendff("{: >{}}INVERT:\n", "", indent); @@ -543,6 +644,57 @@ CalculatedStyleValue::CalculationResult MinCalculationNode::resolve(CalculationR return smallest_node; } +NonnullRefPtr MinCalculationNode::with_simplified_children(CalculationContext const& context, CalculationResolutionContext const& resolution_context) const +{ + return simplify_children_vector(*this, context, resolution_context); +} + +// https://drafts.csswg.org/css-values-4/#funcdef-min +enum class MinOrMax { + Min, + Max, +}; +static Optional run_min_or_max_operation_if_possible(Vector> const& children, CalculationContext const& context, CalculationResolutionContext const& resolution_context, MinOrMax min_or_max) +{ + // The min() or max() functions contain one or more comma-separated calculations, and represent the smallest + // (most negative) or largest (most positive) of them, respectively. + Optional result; + for (auto const& child : children) { + auto child_value = try_get_value_with_canonical_unit(child, context, resolution_context); + if (!child_value.has_value()) + return {}; + + if (!result.has_value()) { + result = child_value.release_value(); + } else { + auto consistent_type = result->type()->consistent_type(child_value->type().value()); + if (!consistent_type.has_value()) + return {}; + + if (min_or_max == MinOrMax::Min) { + if (child_value->value() < result->value()) { + result = CalculatedStyleValue::CalculationResult { child_value->value(), consistent_type }; + } else { + result = CalculatedStyleValue::CalculationResult { result->value(), consistent_type }; + } + } else { + if (child_value->value() > result->value()) { + result = CalculatedStyleValue::CalculationResult { child_value->value(), consistent_type }; + } else { + result = CalculatedStyleValue::CalculationResult { result->value(), consistent_type }; + } + } + } + } + return result; +} + +// https://drafts.csswg.org/css-values-4/#funcdef-min +Optional MinCalculationNode::run_operation_if_possible(CalculationContext const& context, CalculationResolutionContext const& resolution_context) const +{ + return run_min_or_max_operation_if_possible(m_values, context, resolution_context, MinOrMax::Min); +} + void MinCalculationNode::dump(StringBuilder& builder, int indent) const { builder.appendff("{: >{}}MIN:\n", "", indent); @@ -622,6 +774,17 @@ CalculatedStyleValue::CalculationResult MaxCalculationNode::resolve(CalculationR return largest_node; } +NonnullRefPtr MaxCalculationNode::with_simplified_children(CalculationContext const& context, CalculationResolutionContext const& resolution_context) const +{ + return simplify_children_vector(*this, context, resolution_context); +} + +// https://drafts.csswg.org/css-values-4/#funcdef-max +Optional MaxCalculationNode::run_operation_if_possible(CalculationContext const& context, CalculationResolutionContext const& resolution_context) const +{ + return run_min_or_max_operation_if_possible(m_values, context, resolution_context, MinOrMax::Max); +} + void MaxCalculationNode::dump(StringBuilder& builder, int indent) const { builder.appendff("{: >{}}MAX:\n", "", indent); @@ -702,6 +865,51 @@ CalculatedStyleValue::CalculationResult ClampCalculationNode::resolve(Calculatio VERIFY_NOT_REACHED(); } +NonnullRefPtr ClampCalculationNode::with_simplified_children(CalculationContext const& context, CalculationResolutionContext const& resolution_context) const +{ + auto simplified_min = simplify_a_calculation_tree(m_min_value, context, resolution_context); + auto simplified_center = simplify_a_calculation_tree(m_center_value, context, resolution_context); + auto simplified_max = simplify_a_calculation_tree(m_max_value, context, resolution_context); + if (simplified_min != m_min_value || simplified_center != m_center_value || simplified_max != m_max_value) + return create(move(simplified_min), move(simplified_center), move(simplified_max)); + return *this; +} + +// https://drafts.csswg.org/css-values-4/#funcdef-clamp +Optional ClampCalculationNode::run_operation_if_possible(CalculationContext const& context, CalculationResolutionContext const& resolution_context) const +{ + // The clamp() function takes three calculations — a minimum value, a central value, and a maximum value — and + // represents its central calculation, clamped according to its min and max calculations, favoring the min + // calculation if it conflicts with the max. (That is, given clamp(MIN, VAL, MAX), it represents exactly the + // same value as max(MIN, min(VAL, MAX))). + // + // Either the min or max calculations (or even both) can instead be the keyword none, which indicates the value + // is not clamped from that side. (That is, clamp(MIN, VAL, none) is equivalent to max(MIN, VAL), clamp(none, + // VAL, MAX) is equivalent to min(VAL, MAX), and clamp(none, VAL, none) is equivalent to just calc(VAL).) + // + // For all three functions, the argument calculations can resolve to any , , or , + // but must have a consistent type or else the function is invalid; the result’s type will be the consistent type. + + auto min_result = try_get_value_with_canonical_unit(m_min_value, context, resolution_context); + if (!min_result.has_value()) + return {}; + + auto center_result = try_get_value_with_canonical_unit(m_center_value, context, resolution_context); + if (!center_result.has_value()) + return {}; + + auto max_result = try_get_value_with_canonical_unit(m_max_value, context, resolution_context); + if (!max_result.has_value()) + return {}; + + auto consistent_type = min_result->type()->consistent_type(center_result->type().value()).map([&](auto& it) { return it.consistent_type(max_result->type().value()); }); + if (!consistent_type.has_value()) + return {}; + + auto chosen_value = max(min_result->value(), min(center_result->value(), max_result->value())); + return CalculatedStyleValue::CalculationResult { chosen_value, consistent_type.release_value() }; +} + void ClampCalculationNode::dump(StringBuilder& builder, int indent) const { builder.appendff("{: >{}}CLAMP:\n", "", indent); @@ -758,6 +966,22 @@ CalculatedStyleValue::CalculationResult AbsCalculationNode::resolve(CalculationR return node_a; } +NonnullRefPtr AbsCalculationNode::with_simplified_children(CalculationContext const& context, CalculationResolutionContext const& resolution_context) const +{ + return simplify_child(*this, m_value, context, resolution_context); +} + +// https://drafts.csswg.org/css-values-4/#funcdef-abs +Optional AbsCalculationNode::run_operation_if_possible(CalculationContext const& context, CalculationResolutionContext const& resolution_context) const +{ + // The abs(A) function contains one calculation A, and returns the absolute value of A, as the same type as the input: + // if A’s numeric value is positive or 0⁺, just A again; otherwise -1 * A. + auto child_value = try_get_value_with_canonical_unit(m_value, context, resolution_context); + if (!child_value.has_value()) + return {}; + return CalculatedStyleValue::CalculationResult { fabs(child_value->value()), child_value->type() }; +} + void AbsCalculationNode::dump(StringBuilder& builder, int indent) const { builder.appendff("{: >{}}ABS: {}\n", "", indent, to_string()); @@ -815,6 +1039,34 @@ CalculatedStyleValue::CalculationResult SignCalculationNode::resolve(Calculation return { 0, CSSNumericType {} }; } +NonnullRefPtr SignCalculationNode::with_simplified_children(CalculationContext const& context, CalculationResolutionContext const& resolution_context) const +{ + return simplify_child(*this, m_value, context, resolution_context); +} + +// https://drafts.csswg.org/css-values-4/#funcdef-sign +Optional SignCalculationNode::run_operation_if_possible(CalculationContext const& context, CalculationResolutionContext const& resolution_context) const +{ + // The sign(A) function contains one calculation A, and returns -1 if A’s numeric value is negative, + // +1 if A’s numeric value is positive, 0⁺ if A’s numeric value is 0⁺, and 0⁻ if A’s numeric value is 0⁻. + // The return type is a , made consistent with the input calculation’s type. + auto child_value = try_get_value_with_canonical_unit(m_value, context, resolution_context); + if (!child_value.has_value()) + return {}; + + double sign = 0; + if (child_value->value() < 0) { + sign = -1; + } else if (child_value->value() > 0) { + sign = 1; + } else { + FloatExtractor const extractor { .d = child_value->value() }; + sign = extractor.sign ? -0 : 0; + } + + return CalculatedStyleValue::CalculationResult { sign, CSSNumericType {}.made_consistent_with(child_value->type().value()) }; +} + void SignCalculationNode::dump(StringBuilder& builder, int indent) const { builder.appendff("{: >{}}SIGN: {}\n", "", indent, to_string()); @@ -933,6 +1185,56 @@ CalculatedStyleValue::CalculationResult SinCalculationNode::resolve(CalculationR return { result, CSSNumericType {} }; } +NonnullRefPtr SinCalculationNode::with_simplified_children(CalculationContext const& context, CalculationResolutionContext const& resolution_context) const +{ + return simplify_child(*this, m_value, context, resolution_context); +} + +enum class SinCosOrTan { + Sin, + Cos, + Tan, +}; +static Optional run_sin_cos_or_tan_operation_if_possible(CalculationNode const& child, SinCosOrTan trig_function) +{ + // The sin(A), cos(A), and tan(A) functions all contain a single calculation which must resolve to either a + // or an , and compute their corresponding function by interpreting the result of their argument as radians. + // (That is, sin(45deg), sin(.125turn), and sin(3.14159 / 4) all represent the same value, approximately .707.) They + // all represent a , with the return type made consistent with the input calculation’s type. sin() and cos() + // will always return a number between −1 and 1, while tan() can return any number between −∞ and +∞. + // (See § 10.9 Type Checking for details on how math functions handle ∞.) + + if (child.type() != CalculationNode::Type::Numeric) + return {}; + auto const& numeric_child = as(child); + + auto radians = numeric_child.value().visit( + [](Angle const& angle) { return angle.to_radians(); }, + [](Number const& number) { return number.value(); }, + [](auto const&) -> double { VERIFY_NOT_REACHED(); }); + + double result = 0; + switch (trig_function) { + case SinCosOrTan::Sin: + result = sin(radians); + break; + case SinCosOrTan::Cos: + result = cos(radians); + break; + case SinCosOrTan::Tan: + result = tan(radians); + break; + } + + return CalculatedStyleValue::CalculationResult { result, CSSNumericType {}.made_consistent_with(child.numeric_type().value()) }; +} + +// https://drafts.csswg.org/css-values-4/#funcdef-sin +Optional SinCalculationNode::run_operation_if_possible(CalculationContext const&, CalculationResolutionContext const&) const +{ + return run_sin_cos_or_tan_operation_if_possible(m_value, SinCosOrTan::Sin); +} + void SinCalculationNode::dump(StringBuilder& builder, int indent) const { builder.appendff("{: >{}}SIN: {}\n", "", indent, to_string()); @@ -985,6 +1287,17 @@ CalculatedStyleValue::CalculationResult CosCalculationNode::resolve(CalculationR return { result, CSSNumericType {} }; } +NonnullRefPtr CosCalculationNode::with_simplified_children(CalculationContext const& context, CalculationResolutionContext const& resolution_context) const +{ + return simplify_child(*this, m_value, context, resolution_context); +} + +// https://drafts.csswg.org/css-values-4/#funcdef-cos +Optional CosCalculationNode::run_operation_if_possible(CalculationContext const&, CalculationResolutionContext const&) const +{ + return run_sin_cos_or_tan_operation_if_possible(m_value, SinCosOrTan::Cos); +} + void CosCalculationNode::dump(StringBuilder& builder, int indent) const { builder.appendff("{: >{}}COS: {}\n", "", indent, to_string()); @@ -1037,6 +1350,17 @@ CalculatedStyleValue::CalculationResult TanCalculationNode::resolve(CalculationR return { result, CSSNumericType {} }; } +NonnullRefPtr TanCalculationNode::with_simplified_children(CalculationContext const& context, CalculationResolutionContext const& resolution_context) const +{ + return simplify_child(*this, m_value, context, resolution_context); +} + +// https://drafts.csswg.org/css-values-4/#funcdef-tan +Optional TanCalculationNode::run_operation_if_possible(CalculationContext const&, CalculationResolutionContext const&) const +{ + return run_sin_cos_or_tan_operation_if_possible(m_value, SinCosOrTan::Tan); +} + void TanCalculationNode::dump(StringBuilder& builder, int indent) const { builder.appendff("{: >{}}TAN: {}\n", "", indent, to_string()); @@ -1087,6 +1411,61 @@ CalculatedStyleValue::CalculationResult AsinCalculationNode::resolve(Calculation return { result, CSSNumericType { CSSNumericType::BaseType::Angle, 1 } }; } +NonnullRefPtr AsinCalculationNode::with_simplified_children(CalculationContext const& context, CalculationResolutionContext const& resolution_context) const +{ + return simplify_child(*this, m_value, context, resolution_context); +} + +enum class AsinAcosOrAtan { + Asin, + Acos, + Atan, +}; +static Optional run_asin_acos_or_atan_operation_if_possible(CalculationNode const& child, AsinAcosOrAtan trig_function) +{ + // The asin(A), acos(A), and atan(A) functions are the "arc" or "inverse" trigonometric functions, representing + // the inverse function to their corresponding "normal" trig functions. All of them contain a single calculation + // which must resolve to a , and compute their corresponding function, interpreting their result as a + // number of radians, representing an with the return type made consistent with the input calculation’s + // type. The angle returned by asin() must be normalized to the range [-90deg, 90deg]; the angle returned by acos() + // to the range [0deg, 180deg]; and the angle returned by atan() to the range [-90deg, 90deg]. + + auto maybe_number = try_get_number(child); + if (!maybe_number.has_value()) + return {}; + auto number = maybe_number.release_value(); + + auto normalize_angle = [](double radians, double min_degrees, double max_degrees) -> double { + auto degrees = AK::to_degrees(radians); + while (degrees < min_degrees) + degrees += 360; + while (degrees > max_degrees) + degrees -= 360; + return degrees; + }; + + double result = 0; + switch (trig_function) { + case AsinAcosOrAtan::Asin: + result = normalize_angle(asin(number), -90, 90); + break; + case AsinAcosOrAtan::Acos: + result = normalize_angle(acos(number), 0, 180); + break; + case AsinAcosOrAtan::Atan: + result = normalize_angle(atan(number), -90, 90); + break; + } + + return CalculatedStyleValue::CalculationResult { result, CSSNumericType {}.made_consistent_with(child.numeric_type().value()) }; +} + +// https://drafts.csswg.org/css-values-4/#funcdef-asin +Optional AsinCalculationNode::run_operation_if_possible(CalculationContext const&, CalculationResolutionContext const&) const +{ + return run_asin_acos_or_atan_operation_if_possible(m_value, AsinAcosOrAtan::Asin); +} + void AsinCalculationNode::dump(StringBuilder& builder, int indent) const { builder.appendff("{: >{}}ASIN: {}\n", "", indent, to_string()); @@ -1137,6 +1516,17 @@ CalculatedStyleValue::CalculationResult AcosCalculationNode::resolve(Calculation return { result, CSSNumericType { CSSNumericType::BaseType::Angle, 1 } }; } +NonnullRefPtr AcosCalculationNode::with_simplified_children(CalculationContext const& context, CalculationResolutionContext const& resolution_context) const +{ + return simplify_child(*this, m_value, context, resolution_context); +} + +// https://drafts.csswg.org/css-values-4/#funcdef-acos +Optional AcosCalculationNode::run_operation_if_possible(CalculationContext const&, CalculationResolutionContext const&) const +{ + return run_asin_acos_or_atan_operation_if_possible(m_value, AsinAcosOrAtan::Acos); +} + void AcosCalculationNode::dump(StringBuilder& builder, int indent) const { builder.appendff("{: >{}}ACOS: {}\n", "", indent, to_string()); @@ -1187,6 +1577,17 @@ CalculatedStyleValue::CalculationResult AtanCalculationNode::resolve(Calculation return { result, CSSNumericType { CSSNumericType::BaseType::Angle, 1 } }; } +NonnullRefPtr AtanCalculationNode::with_simplified_children(CalculationContext const& context, CalculationResolutionContext const& resolution_context) const +{ + return simplify_child(*this, m_value, context, resolution_context); +} + +// https://drafts.csswg.org/css-values-4/#funcdef-atan +Optional AtanCalculationNode::run_operation_if_possible(CalculationContext const&, CalculationResolutionContext const&) const +{ + return run_asin_acos_or_atan_operation_if_possible(m_value, AsinAcosOrAtan::Atan); +} + void AtanCalculationNode::dump(StringBuilder& builder, int indent) const { builder.appendff("{: >{}}ATAN: {}\n", "", indent, to_string()); @@ -1241,6 +1642,39 @@ CalculatedStyleValue::CalculationResult Atan2CalculationNode::resolve(Calculatio return { result, CSSNumericType { CSSNumericType::BaseType::Angle, 1 } }; } +NonnullRefPtr Atan2CalculationNode::with_simplified_children(CalculationContext const& context, CalculationResolutionContext const& resolution_context) const +{ + return simplify_2_children(*this, m_x, m_y, context, resolution_context); +} + +// https://drafts.csswg.org/css-values-4/#funcdef-atan2 +Optional Atan2CalculationNode::run_operation_if_possible(CalculationContext const& context, CalculationResolutionContext const& resolution_context) const +{ + // The atan2(A, B) function contains two comma-separated calculations, A and B. A and B can resolve to any , + // , or , but must have a consistent type or else the function is invalid. The function + // returns the between the positive X-axis and the point (B,A), with the return type made consistent with the + // input calculation’s type. The returned angle must be normalized to the interval (-180deg, 180deg] (that is, + // greater than -180deg, and less than or equal to 180deg). + auto x_value = try_get_value_with_canonical_unit(m_x, context, resolution_context); + if (!x_value.has_value()) + return {}; + auto y_value = try_get_value_with_canonical_unit(m_y, context, resolution_context); + if (!y_value.has_value()) + return {}; + + auto input_consistent_type = x_value->type()->consistent_type(y_value->type().value()); + if (!input_consistent_type.has_value()) + return {}; + + auto degrees = AK::to_degrees(atan2(y_value->value(), x_value->value())); + while (degrees <= -180) + degrees += 360; + while (degrees > 180) + degrees -= 360; + + return CalculatedStyleValue::CalculationResult { degrees, CSSNumericType { CSSNumericType::BaseType::Angle, 1 }.made_consistent_with(*input_consistent_type) }; +} + void Atan2CalculationNode::dump(StringBuilder& builder, int indent) const { builder.appendff("{: >{}}ATAN2: {}\n", "", indent, to_string()); @@ -1291,6 +1725,29 @@ CalculatedStyleValue::CalculationResult PowCalculationNode::resolve(CalculationR return { result, CSSNumericType {} }; } +NonnullRefPtr PowCalculationNode::with_simplified_children(CalculationContext const& context, CalculationResolutionContext const& resolution_context) const +{ + return simplify_2_children(*this, m_x, m_y, context, resolution_context); +} + +// https://drafts.csswg.org/css-values-4/#funcdef-pow +Optional PowCalculationNode::run_operation_if_possible(CalculationContext const&, CalculationResolutionContext const&) const +{ + // The pow(A, B) function contains two comma-separated calculations A and B, both of which must resolve to s, + // and returns the result of raising A to the power of B, returning the value as a . The input calculations + // must have a consistent type or else the function is invalid; the result’s type will be the consistent type. + auto a = try_get_number(m_x); + auto b = try_get_number(m_y); + if (!a.has_value() || !b.has_value()) + return {}; + + auto consistent_type = m_x->numeric_type()->consistent_type(m_y->numeric_type().value()); + if (!consistent_type.has_value()) + return {}; + + return CalculatedStyleValue::CalculationResult { pow(*a, *b), consistent_type }; +} + void PowCalculationNode::dump(StringBuilder& builder, int indent) const { builder.appendff("{: >{}}POW: {}\n", "", indent, to_string()); @@ -1337,6 +1794,29 @@ CalculatedStyleValue::CalculationResult SqrtCalculationNode::resolve(Calculation return { result, CSSNumericType {} }; } +NonnullRefPtr SqrtCalculationNode::with_simplified_children(CalculationContext const& context, CalculationResolutionContext const& resolution_context) const +{ + return simplify_child(*this, m_value, context, resolution_context); +} + +// https://drafts.csswg.org/css-values-4/#funcdef-sqrt +Optional SqrtCalculationNode::run_operation_if_possible(CalculationContext const&, CalculationResolutionContext const&) const +{ + // The sqrt(A) function contains a single calculation which must resolve to a , and returns the square root + // of the value as a , with the return type made consistent with the input calculation’s type. + // (sqrt(X) and pow(X, .5) are basically equivalent, differing only in some error-handling; sqrt() is a common enough + // function that it is provided as a convenience.) + auto number = try_get_number(m_value); + if (!number.has_value()) + return {}; + + auto consistent_type = CSSNumericType {}.made_consistent_with(m_value->numeric_type().value()); + if (!consistent_type.has_value()) + return {}; + + return CalculatedStyleValue::CalculationResult { sqrt(*number), consistent_type }; +} + void SqrtCalculationNode::dump(StringBuilder& builder, int indent) const { builder.appendff("{: >{}}SQRT: {}\n", "", indent, to_string()); @@ -1411,6 +1891,39 @@ CalculatedStyleValue::CalculationResult HypotCalculationNode::resolve(Calculatio return { result, result_type }; } +NonnullRefPtr HypotCalculationNode::with_simplified_children(CalculationContext const& context, CalculationResolutionContext const& resolution_context) const +{ + return simplify_children_vector(*this, context, resolution_context); +} + +// https://drafts.csswg.org/css-values-4/#funcdef-hypot +Optional HypotCalculationNode::run_operation_if_possible(CalculationContext const& context, CalculationResolutionContext const& resolution_context) const +{ + // The hypot(A, …) function contains one or more comma-separated calculations, and returns the length of an + // N-dimensional vector with components equal to each of the calculations. (That is, the square root of the sum of + // the squares of its arguments.) The argument calculations can resolve to any , , or + // , but must have a consistent type or else the function is invalid; the result’s type will be the + // consistent type. + + CSSNumericType consistent_type; + double value = 0; + + for (auto const& child : m_values) { + auto canonical_child = try_get_value_with_canonical_unit(child, context, resolution_context); + if (!canonical_child.has_value()) + return {}; + + auto maybe_type = consistent_type.consistent_type(canonical_child->type().value()); + if (!maybe_type.has_value()) + return {}; + + consistent_type = maybe_type.release_value(); + value += canonical_child->value() * canonical_child->value(); + } + + return CalculatedStyleValue::CalculationResult { sqrt(value), consistent_type }; +} + void HypotCalculationNode::dump(StringBuilder& builder, int indent) const { builder.appendff("{: >{}}HYPOT:\n", "", indent); @@ -1466,6 +1979,30 @@ CalculatedStyleValue::CalculationResult LogCalculationNode::resolve(CalculationR return { result, CSSNumericType {} }; } +NonnullRefPtr LogCalculationNode::with_simplified_children(CalculationContext const& context, CalculationResolutionContext const& resolution_context) const +{ + return simplify_2_children(*this, m_x, m_y, context, resolution_context); +} + +// https://drafts.csswg.org/css-values-4/#funcdef-log +Optional LogCalculationNode::run_operation_if_possible(CalculationContext const&, CalculationResolutionContext const&) const +{ + // The log(A, B?) function contains one or two calculations (representing the value to be logarithmed, and the + // base of the logarithm, defaulting to e), which must resolve to s, and returns the logarithm base B of + // the value A, as a with the return type made consistent with the input calculation’s type. + + auto number = try_get_number(m_x); + auto base = try_get_number(m_y); + if (!number.has_value() || !base.has_value()) + return {}; + + auto consistent_type = CSSNumericType {}.made_consistent_with(m_x->numeric_type().value()); + if (!consistent_type.has_value()) + return {}; + + return CalculatedStyleValue::CalculationResult { log(*number) / log(*base), consistent_type }; +} + void LogCalculationNode::dump(StringBuilder& builder, int indent) const { builder.appendff("{: >{}}LOG: {}\n", "", indent, to_string()); @@ -1512,6 +2049,28 @@ CalculatedStyleValue::CalculationResult ExpCalculationNode::resolve(CalculationR return { result, CSSNumericType {} }; } +NonnullRefPtr ExpCalculationNode::with_simplified_children(CalculationContext const& context, CalculationResolutionContext const& resolution_context) const +{ + return simplify_child(*this, m_value, context, resolution_context); +} + +// https://drafts.csswg.org/css-values-4/#funcdef-exp +Optional ExpCalculationNode::run_operation_if_possible(CalculationContext const&, CalculationResolutionContext const&) const +{ + // The exp(A) function contains one calculation which must resolve to a , and returns the same value as + // pow(e, A) as a with the return type made consistent with the input calculation’s type. + + auto number = try_get_number(m_value); + if (!number.has_value()) + return {}; + + auto consistent_type = CSSNumericType {}.made_consistent_with(m_value->numeric_type().value()); + if (!consistent_type.has_value()) + return {}; + + return CalculatedStyleValue::CalculationResult { exp(*number), consistent_type }; +} + void ExpCalculationNode::dump(StringBuilder& builder, int indent) const { builder.appendff("{: >{}}EXP: {}\n", "", indent, to_string()); @@ -1600,6 +2159,88 @@ CalculatedStyleValue::CalculationResult RoundCalculationNode::resolve(Calculatio VERIFY_NOT_REACHED(); } +NonnullRefPtr RoundCalculationNode::with_simplified_children(CalculationContext const& context, CalculationResolutionContext const& resolution_context) const +{ + auto simplified_x = simplify_a_calculation_tree(m_x, context, resolution_context); + auto simplified_y = simplify_a_calculation_tree(m_y, context, resolution_context); + if (simplified_x != m_x || simplified_y != m_y) + return create(m_strategy, move(simplified_x), move(simplified_y)); + return *this; +} + +// https://drafts.csswg.org/css-values-4/#funcdef-round +Optional RoundCalculationNode::run_operation_if_possible(CalculationContext const& context, CalculationResolutionContext const& resolution_context) const +{ + // The round(?, A, B?) function contains an optional rounding strategy, and two calculations A + // and B, and returns the value of A, rounded according to the rounding strategy, to the nearest integer multiple of + // B either above or below A. The argument calculations can resolve to any , , or , + // but must have a consistent type or else the function is invalid; the result’s type will be the consistent type. + + auto maybe_a = try_get_value_with_canonical_unit(m_x, context, resolution_context); + auto maybe_b = try_get_value_with_canonical_unit(m_y, context, resolution_context); + if (!maybe_a.has_value() || !maybe_b.has_value()) + return {}; + + auto consistent_type = maybe_a->type()->made_consistent_with(maybe_b->type().value()); + if (!consistent_type.has_value()) + return {}; + + auto a = maybe_a->value(); + auto b = maybe_b->value(); + + // If A is exactly equal to an integer multiple of B, round() resolves to A exactly (preserving whether A is 0⁻ or + // 0⁺, if relevant). + if (fmod(a, b) == 0) + return maybe_a.release_value(); + + // Otherwise, there are two integer multiples of B that are potentially "closest" to A, lower B which is closer to + // −∞ and upper B which is closer to +∞. The following s dictate how to choose between them: + + // FIXME: If lower B would be zero, it is specifically equal to 0⁺; + // if upper B would be zero, it is specifically equal to 0⁻. + auto get_lower_b = [&]() { + return floor(a / b) * b; + }; + auto get_upper_b = [&]() { + return ceil(a / b) * b; + }; + + double rounded = 0; + switch (m_strategy) { + // -> nearest + case RoundingStrategy::Nearest: { + // Choose whichever of lower B and upper B that has the smallest absolute difference from A. + // If both have an equal difference (A is exactly between the two values), choose upper B. + auto lower_b = get_lower_b(); + auto upper_b = get_upper_b(); + auto lower_diff = fabs(lower_b - a); + auto upper_diff = fabs(upper_b - a); + rounded = upper_diff <= lower_diff ? upper_b : lower_b; + break; + } + // -> up + case RoundingStrategy::Up: + // Choose upper B. + rounded = get_upper_b(); + break; + // -> down + case RoundingStrategy::Down: + // Choose lower B. + rounded = get_lower_b(); + break; + // -> to-zero + case RoundingStrategy::ToZero: { + // Choose whichever of lower B and upper B that has the smallest absolute difference from 0. + auto lower_b = get_lower_b(); + auto upper_b = get_upper_b(); + rounded = fabs(upper_b) < fabs(lower_b) ? upper_b : lower_b; + break; + } + } + + return CalculatedStyleValue::CalculationResult { rounded, consistent_type }; +} + void RoundCalculationNode::dump(StringBuilder& builder, int indent) const { builder.appendff("{: >{}}ROUND: {}\n", "", indent, to_string()); @@ -1662,6 +2303,57 @@ CalculatedStyleValue::CalculationResult ModCalculationNode::resolve(CalculationR return { value, node_a.type() }; } +NonnullRefPtr ModCalculationNode::with_simplified_children(CalculationContext const& context, CalculationResolutionContext const& resolution_context) const +{ + return simplify_2_children(*this, m_x, m_y, context, resolution_context); +} + +enum class ModOrRem { + Mod, + Rem, +}; +// https://drafts.csswg.org/css-values-4/#funcdef-mod +static Optional run_mod_or_rem_operation_if_possible(CalculationNode const& numerator, CalculationNode const& denominator, CalculationContext const& context, CalculationResolutionContext const& resolution_context, ModOrRem mod_or_rem) +{ + // The modulus functions mod(A, B) and rem(A, B) similarly contain two calculations A and B, and return the + // difference between A and the nearest integer multiple of B either above or below A. The argument calculations + // can resolve to any , , or , but must have the same type, or else the function + // is invalid; the result will have the same type as the arguments. + auto numerator_value = try_get_value_with_canonical_unit(numerator, context, resolution_context); + auto denominator_value = try_get_value_with_canonical_unit(denominator, context, resolution_context); + if (!numerator_value.has_value() || !denominator_value.has_value()) + return {}; + + if (numerator_value->type() != denominator_value->type()) + return {}; + + // The two functions are very similar, and in fact return identical results if both arguments are positive or both + // are negative: the value of the function is equal to the value of A shifted by the integer multiple of B that + // brings the value between zero and B. (Specifically, the range includes zero and excludes B.More specifically, + // if B is positive the range starts at 0⁺, and if B is negative it starts at 0⁻.) + // + // Their behavior diverges if the A value and the B step are on opposite sides of zero: mod() (short for “modulus”) + // continues to choose the integer multiple of B that puts the value between zero and B, as above (guaranteeing + // that the result will either be zero or share the sign of B, not A), while rem() (short for "remainder") chooses + // the integer multiple of B that puts the value between zero and -B, avoiding changing the sign of the value. + + double result = 0; + if (mod_or_rem == ModOrRem::Mod) { + auto quotient = floor(numerator_value->value() / denominator_value->value()); + result = numerator_value->value() - (denominator_value->value() * quotient); + } else { + result = fmod(numerator_value->value(), denominator_value->value()); + } + + return CalculatedStyleValue::CalculationResult { result, numerator_value->type() }; +} + +// https://drafts.csswg.org/css-values-4/#funcdef-mod +Optional ModCalculationNode::run_operation_if_possible(CalculationContext const& context, CalculationResolutionContext const& resolution_context) const +{ + return run_mod_or_rem_operation_if_possible(m_x, m_y, context, resolution_context, ModOrRem::Mod); +} + void ModCalculationNode::dump(StringBuilder& builder, int indent) const { builder.appendff("{: >{}}MOD: {}\n", "", indent, to_string()); @@ -1718,6 +2410,17 @@ CalculatedStyleValue::CalculationResult RemCalculationNode::resolve(CalculationR return { value, node_a.type() }; } +NonnullRefPtr RemCalculationNode::with_simplified_children(CalculationContext const& context, CalculationResolutionContext const& resolution_context) const +{ + return simplify_2_children(*this, m_x, m_y, context, resolution_context); +} + +// https://drafts.csswg.org/css-values-4/#funcdef-mod +Optional RemCalculationNode::run_operation_if_possible(CalculationContext const& context, CalculationResolutionContext const& resolution_context) const +{ + return run_mod_or_rem_operation_if_possible(m_x, m_y, context, resolution_context, ModOrRem::Rem); +} + void RemCalculationNode::dump(StringBuilder& builder, int indent) const { builder.appendff("{: >{}}REM: {}\n", "", indent, to_string()); @@ -1900,4 +2603,522 @@ String CalculatedStyleValue::dump() const return builder.to_string_without_validation(); } +struct NumericChildAndIndex { + NonnullRefPtr child; + size_t index; +}; +static Optional find_numeric_child_with_same_unit(Vector> children, NumericCalculationNode const& target) +{ + for (auto i = 0u; i < children.size(); ++i) { + auto& child = children[i]; + if (child->type() != CalculationNode::Type::Numeric) + continue; + auto const& child_numeric = as(*child); + if (child_numeric.value().index() != target.value().index()) + continue; + + auto matches = child_numeric.value().visit( + [&](Percentage const&) { + return target.value().has(); + }, + [&](Number const&) { + return target.value().has(); + }, + [&](T const& value) { + if (auto const* other = target.value().get_pointer(); other && other->type() == value.type()) { + return true; + } + return false; + }); + + if (matches) + return NumericChildAndIndex { child_numeric, i }; + } + + return {}; +} + +static RefPtr make_calculation_node(CalculatedStyleValue::CalculationResult const& calculation_result, CalculationContext const& context) +{ + auto const& accumulated_type = calculation_result.type().value(); + if (accumulated_type.matches_number(context.percentages_resolve_as)) + return NumericCalculationNode::create(Number { Number::Type::Number, calculation_result.value() }, context); + if (accumulated_type.matches_percentage()) + return NumericCalculationNode::create(Percentage { calculation_result.value() }, context); + if (accumulated_type.matches_angle(context.percentages_resolve_as)) + return NumericCalculationNode::create(Angle::make_degrees(calculation_result.value()), context); + if (accumulated_type.matches_flex(context.percentages_resolve_as)) + return NumericCalculationNode::create(Flex::make_fr(calculation_result.value()), context); + if (accumulated_type.matches_frequency(context.percentages_resolve_as)) + return NumericCalculationNode::create(Frequency::make_hertz(calculation_result.value()), context); + if (accumulated_type.matches_length(context.percentages_resolve_as)) + return NumericCalculationNode::create(Length::make_px(calculation_result.value()), context); + if (accumulated_type.matches_resolution(context.percentages_resolve_as)) + return NumericCalculationNode::create(Resolution::make_dots_per_pixel(calculation_result.value()), context); + if (accumulated_type.matches_time(context.percentages_resolve_as)) + return NumericCalculationNode::create(Time::make_seconds(calculation_result.value()), context); + + return nullptr; +} + +// https://drafts.csswg.org/css-values-4/#calc-simplification +NonnullRefPtr simplify_a_calculation_tree(CalculationNode const& original_root, CalculationContext const& context, CalculationResolutionContext const& resolution_context) +{ + // To simplify a calculation tree root: + // FIXME: If needed, we could detect that nothing has changed and then return the original `root`, in more places. + NonnullRefPtr root = original_root; + + // 1. If root is a numeric value: + if (root->type() == CalculationNode::Type::Numeric) { + auto const& root_numeric = as(*root); + + // 1. If root is a percentage that will be resolved against another value, and there is enough information + // available to resolve it, do so, and express the resulting numeric value in the appropriate canonical unit. + // Return the value. + if (auto const* percentage = root_numeric.value().get_pointer(); percentage && context.percentages_resolve_as.has_value()) { + // NOTE: We use nullptr here to signify "use the original". + RefPtr resolved = resolution_context.percentage_basis.visit( + [](Empty const&) -> RefPtr { return nullptr; }, + [&](Angle const& angle) -> RefPtr { + VERIFY(context.percentages_resolve_as == ValueType::Angle); + if (angle.type() == Angle::Type::Deg) + return nullptr; + return NumericCalculationNode::create(Angle::make_degrees(angle.to_degrees()).percentage_of(*percentage), context); + }, + [&](Frequency const& frequency) -> RefPtr { + VERIFY(context.percentages_resolve_as == ValueType::Frequency); + if (frequency.type() == Frequency::Type::Hz) + return nullptr; + return NumericCalculationNode::create(Frequency::make_hertz(frequency.to_hertz()).percentage_of(*percentage), context); + }, + [&](Length const& length) -> RefPtr { + VERIFY(context.percentages_resolve_as == ValueType::Length); + if (length.type() == Length::Type::Px) + return nullptr; + if (length.is_absolute()) + return NumericCalculationNode::create(Length::make_px(length.absolute_length_to_px()).percentage_of(*percentage), context); + if (resolution_context.length_resolution_context.has_value()) + return NumericCalculationNode::create(Length::make_px(length.to_px(resolution_context.length_resolution_context.value())), context); + return nullptr; + }, + [&](Time const& time) -> RefPtr { + VERIFY(context.percentages_resolve_as == ValueType::Time); + if (time.type() == Time::Type::S) + return nullptr; + return NumericCalculationNode::create(Time::make_seconds(time.to_seconds()).percentage_of(*percentage), context); + }); + + if (resolved) + return resolved.release_nonnull(); + } + + // 2. If root is a dimension that is not expressed in its canonical unit, and there is enough information available + // to convert it to the canonical unit, do so, and return the value. + else { + // NOTE: We use nullptr here to signify "use the original". + RefPtr resolved = root_numeric.value().visit( + [&](Angle const& angle) -> RefPtr { + if (angle.type() == Angle::Type::Deg) + return nullptr; + return NumericCalculationNode::create(Angle::make_degrees(angle.to_degrees()), context); + }, + [&](Flex const& flex) -> RefPtr { + if (flex.type() == Flex::Type::Fr) + return nullptr; + return NumericCalculationNode::create(Flex::make_fr(flex.to_fr()), context); + }, + [&](Frequency const& frequency) -> RefPtr { + if (frequency.type() == Frequency::Type::Hz) + return nullptr; + return NumericCalculationNode::create(Frequency::make_hertz(frequency.to_hertz()), context); + }, + [&](Length const& length) -> RefPtr { + if (length.type() == Length::Type::Px) + return nullptr; + if (length.is_absolute()) + return NumericCalculationNode::create(Length::make_px(length.absolute_length_to_px()), context); + if (resolution_context.length_resolution_context.has_value()) + return NumericCalculationNode::create(Length::make_px(length.to_px(resolution_context.length_resolution_context.value())), context); + return nullptr; + }, + [&](Number const&) -> RefPtr { + return nullptr; + }, + [&](Percentage const&) -> RefPtr { + return nullptr; + }, + [&](Resolution const& resolution) -> RefPtr { + if (resolution.type() == Resolution::Type::Dppx) + return nullptr; + return NumericCalculationNode::create(Resolution::make_dots_per_pixel(resolution.to_dots_per_pixel()), context); + }, + [&](Time const& time) -> RefPtr { + if (time.type() == Time::Type::S) + return nullptr; + return NumericCalculationNode::create(Time::make_seconds(time.to_seconds()), context); + }); + if (resolved) + return resolved.release_nonnull(); + } + + // 3. If root is a that can be resolved, return what it resolves to, simplified. + // NOTE: This is handled below. + + // 4. Otherwise, return root. + return root; + } + + // AD-HOC: Step 1.3 is done here as we have a separate node type for them. + if (root->type() == CalculationNode::Type::Constant) { + auto const& root_constant = as(*root); + + // 3. If root is a that can be resolved, return what it resolves to, simplified. + // FIXME: At the moment these are all constant numbers. Revisit this once that's not the case. + // (Notably, relative-color syntax allows some other keywords that are relative to the color.) + auto resolved = root_constant.resolve(resolution_context); + VERIFY(resolved.type()->matches_number({})); + return NumericCalculationNode::create(Number { Number::Type::Number, resolved.value() }, context); + } + + // 2. If root is any other leaf node (not an operator node): + // FIXME: We don't yet allow any of these inside a calculation tree. Revisit once we do. + + // 3. At this point, root is an operator node. Simplify all the calculation children of root. + root = root->with_simplified_children(context, resolution_context); + + // 4. If root is an operator node that’s not one of the calc-operator nodes, and all of its calculation children + // are numeric values with enough information to compute the operation root represents, return the result of + // running root’s operation using its children, expressed in the result’s canonical unit. + if (root->is_math_function_node()) { + if (auto maybe_simplified = root->run_operation_if_possible(context, resolution_context); maybe_simplified.has_value()) { + // NOTE: If this returns nullptr, that's a logic error in the code, so it's fine to assert that it's nonnull. + return make_calculation_node(maybe_simplified.release_value(), context).release_nonnull(); + } + } + + // 5. If root is a Min or Max node, attempt to partially simplify it: + if (root->type() == CalculationNode::Type::Min || root->type() == CalculationNode::Type::Max) { + bool const is_min = root->type() == CalculationNode::Type::Min; + auto const& children = is_min ? as(*root).children() : as(*root).children(); + + // 1. For each node child of root’s children: + // If child is a numeric value with enough information to compare magnitudes with another child of the same + // unit (see note in previous step), and there are other children of root that are numeric values with the + // same unit, combine all such children with the appropriate operator per root, and replace child with the + // result, removing all other child nodes involved. + Vector> simplified_children; + simplified_children.ensure_capacity(children.size()); + for (auto const& child : children) { + if (child->type() != CalculationNode::Type::Numeric || simplified_children.is_empty()) { + simplified_children.append(child); + continue; + } + + auto const& child_numeric = as(*child); + if (context.percentages_resolve_as.has_value() && child_numeric.value().has()) { + // NOTE: We can't compare this percentage yet. + simplified_children.append(child); + continue; + } + + auto existing_child_and_index = find_numeric_child_with_same_unit(simplified_children, child_numeric); + if (existing_child_and_index.has_value()) { + bool const should_replace_existing_value = existing_child_and_index->child->value().visit( + [&](Percentage const& percentage) { + if (is_min) + return child_numeric.value().get_pointer()->value() < percentage.value(); + return child_numeric.value().get_pointer()->value() > percentage.value(); + }, + [&](Number const& number) { + if (is_min) + return child_numeric.value().get_pointer()->value() < number.value(); + return child_numeric.value().get_pointer()->value() > number.value(); + }, + [&](T const& value) { + if (is_min) + return child_numeric.value().get_pointer()->raw_value() < value.raw_value(); + return child_numeric.value().get_pointer()->raw_value() > value.raw_value(); + }); + + if (should_replace_existing_value) + simplified_children[existing_child_and_index->index] = child_numeric; + + } else { + simplified_children.append(child); + } + } + + // 2. If root has only one child, return the child. + // Otherwise, return root. + if (simplified_children.size() == 1) + return simplified_children.first(); + // NOTE: Because our root is immutable, we have to return a new node with the modified children. + if (is_min) + return MinCalculationNode::create(move(simplified_children)); + return MaxCalculationNode::create(move(simplified_children)); + } + + // 6. If root is a Negate node: + if (root->type() == CalculationNode::Type::Negate) { + auto const& root_negate = as(*root); + auto const& child = root_negate.child(); + // 1. If root’s child is a numeric value, return an equivalent numeric value, but with the value negated (0 - value). + if (child.type() == CalculationNode::Type::Numeric) { + auto const& numeric_child = as(child); + return numeric_child.value().visit( + [&](Percentage const& percentage) { + return NumericCalculationNode::create(Percentage(-percentage.value()), context); + }, + [&](Number const& number) { + return NumericCalculationNode::create(Number(number.type(), -number.value()), context); + }, + [&](T const& value) { + return NumericCalculationNode::create(T(-value.raw_value(), value.type()), context); + }); + } + + // 2. If root’s child is a Negate node, return the child’s child. + if (child.type() == CalculationNode::Type::Negate) + return as(child).child(); + + // 3. Return root. + // NOTE: Because our root is immutable, we have to return a new node if the child was modified. + if (&child == &root_negate.child()) + return root; + return NegateCalculationNode::create(move(child)); + } + + // 7. If root is an Invert node: + if (root->type() == CalculationNode::Type::Invert) { + auto const& root_invert = as(*root); + auto const& child = root_invert.child(); + + // 1. If root’s child is a number (not a percentage or dimension) return the reciprocal of the child’s value. + if (child.type() == CalculationNode::Type::Numeric) { + if (auto const* number = as(child).value().get_pointer()) { + // TODO: Ensure we're doing the right thing for weird divisions. + return NumericCalculationNode::create(Number(Number::Type::Number, 1.0 / number->value()), context); + } + } + + // 2. If root’s child is an Invert node, return the child’s child. + if (child.type() == CalculationNode::Type::Invert) + return as(child).child(); + + // 3. Return root. + // NOTE: Because our root is immutable, we have to return a new node if the child was modified. + if (&child == &root_invert.child()) + return root; + return InvertCalculationNode::create(move(child)); + } + + // 8. If root is a Sum node: + if (root->type() == CalculationNode::Type::Sum) { + auto const& root_sum = as(*root); + + Vector> flattened_children; + flattened_children.ensure_capacity(root_sum.children().size()); + // 1. For each of root’s children that are Sum nodes, replace them with their children. + for (auto const& child : root_sum.children()) { + if (child->type() == CalculationNode::Type::Sum) { + flattened_children.extend(as(*child).children()); + } else { + flattened_children.append(child); + } + } + + // 2. For each set of root’s children that are numeric values with identical units, remove those children and + // replace them with a single numeric value containing the sum of the removed nodes, and with the same unit. + // (E.g. combine numbers, combine percentages, combine px values, etc.) + + // NOTE: For each child, scan this summed_children list for the first one that has the same type, then replace that with the new summed value. + Vector> summed_children; + for (auto const& child : flattened_children) { + if (child->type() != CalculationNode::Type::Numeric) { + summed_children.append(child); + continue; + } + auto const& child_numeric = as(*child); + + auto existing_child_and_index = find_numeric_child_with_same_unit(summed_children, child_numeric); + if (existing_child_and_index.has_value()) { + auto new_value = existing_child_and_index->child->value().visit( + [&](Percentage const& percentage) { + return NumericCalculationNode::create(Percentage(percentage.value() + child_numeric.value().get().value()), context); + }, + [&](Number const& number) { + return NumericCalculationNode::create(Number(Number::Type::Number, number.value() + child_numeric.value().get().value()), context); + }, + [&](T const& value) { + return NumericCalculationNode::create(T(value.raw_value() + child_numeric.value().get().raw_value(), value.type()), context); + }); + summed_children[existing_child_and_index->index] = move(new_value); + } else { + summed_children.append(child); + } + } + + // 3. If root has only a single child at this point, return the child. Otherwise, return root. + if (summed_children.size() == 1) + return summed_children.first(); + + // NOTE: Because our root is immutable, we have to return a new node with the modified children. + return SumCalculationNode::create(move(summed_children)); + } + + // 9. If root is a Product node: + if (root->type() == CalculationNode::Type::Product) { + auto const& root_product = as(*root); + + Vector> children; + children.ensure_capacity(root_product.children().size()); + + // 1. For each of root’s children that are Product nodes, replace them with their children. + for (auto const& child : root_product.children()) { + if (child->type() == CalculationNode::Type::Product) { + children.extend(as(*child).children()); + } else { + children.append(child); + } + } + + // 2. If root has multiple children that are numbers (not percentages or dimensions), + // remove them and replace them with a single number containing the product of the removed nodes. + Optional number_index; + for (auto i = 0u; i < children.size(); ++i) { + if (children[i]->type() == CalculationNode::Type::Numeric) { + if (auto const* number = as(*children[i]).value().get_pointer()) { + if (!number_index.has_value()) { + number_index = i; + continue; + } + children[*number_index] = NumericCalculationNode::create(as(*children[*number_index]).value().get() * *number, context); + children.remove(i); + --i; // Look at this same index again next loop. + } + } + } + + // 3. If root contains only two children, one of which is a number(not a percentage or dimension) and the other + // of which is a Sum whose children are all numeric values, multiply all of the Sum’ s children by the number, + // then return the Sum. + if (children.size() == 2) { + auto const& child_1 = children[0]; + auto const& child_2 = children[1]; + + Optional multiplier; + RefPtr sum; + + if (child_1->type() == CalculationNode::Type::Numeric && child_2->type() == CalculationNode::Type::Sum) { + if (auto const* maybe_multiplier = as(*child_1).value().get_pointer()) { + multiplier = *maybe_multiplier; + sum = as(*child_2); + } + } + if (child_1->type() == CalculationNode::Type::Sum && child_2->type() == CalculationNode::Type::Numeric) { + if (auto const* maybe_multiplier = as(*child_2).value().get_pointer()) { + multiplier = *maybe_multiplier; + sum = as(*child_1); + } + } + + if (multiplier.has_value() && sum) { + Vector> multiplied_children; + multiplied_children.ensure_capacity(sum->children().size()); + + bool all_numeric = true; + for (auto const& sum_child : sum->children()) { + if (sum_child->type() != CalculationNode::Type::Numeric) { + all_numeric = false; + break; + } + multiplied_children.append(as(*sum_child).value().visit([&](Percentage const& percentage) { return NumericCalculationNode::create(Percentage(percentage.value() * multiplier->value()), context); }, [&](Number const& number) { return NumericCalculationNode::create(Number(Number::Type::Number, number.value() * multiplier->value()), context); }, [&](T const& value) { return NumericCalculationNode::create(T(value.raw_value() * multiplier->value(), value.type()), context); })); + } + + if (all_numeric) + return SumCalculationNode::create(move(multiplied_children)); + } + } + + // 4. If root contains only numeric values and/or Invert nodes containing numeric values, and multiplying the + // types of all the children (noting that the type of an Invert node is the inverse of its child’s type) + // results in a type that matches any of the types that a math function can resolve to, return the result of + // multiplying all the values of the children (noting that the value of an Invert node is the reciprocal of + // its child’s value), expressed in the result’s canonical unit. + Optional accumulated_result; + bool is_valid = true; + for (auto const& child : children) { + if (child->type() == CalculationNode::Type::Numeric) { + auto const& numeric_child = as(*child); + auto child_type = numeric_child.numeric_type(); + if (!child_type.has_value()) { + is_valid = false; + break; + } + + // FIXME: The spec doesn't handle unresolved percentages here, but if we don't exit when we see one, + // we'll get a wrongly-typed value after multiplying the types. + // Spec bug: https://github.com/w3c/csswg-drafts/issues/11588 + if (numeric_child.value().has() && context.percentages_resolve_as.has_value()) { + is_valid = false; + break; + } + + auto child_value = CalculatedStyleValue::CalculationResult::from_value(numeric_child.value(), resolution_context, child_type); + if (accumulated_result.has_value()) { + accumulated_result->multiply_by(child_value); + } else { + accumulated_result = move(child_value); + } + if (!accumulated_result->type().has_value()) { + is_valid = false; + break; + } + continue; + } + if (child->type() == CalculationNode::Type::Invert) { + auto const& invert_child = as(*child); + if (invert_child.child().type() != CalculationNode::Type::Numeric) { + is_valid = false; + break; + } + auto const& grandchild = as(invert_child.child()); + + auto child_type = child->numeric_type(); + if (!child_type.has_value()) { + is_valid = false; + break; + } + + auto child_value = CalculatedStyleValue::CalculationResult::from_value(grandchild.value(), resolution_context, grandchild.numeric_type()); + child_value.invert(); + if (accumulated_result.has_value()) { + accumulated_result->multiply_by(child_value); + } else { + accumulated_result = move(child_value); + } + if (!accumulated_result->type().has_value()) { + is_valid = false; + break; + } + continue; + } + is_valid = false; + break; + } + if (is_valid) { + if (auto node = make_calculation_node(*accumulated_result, context)) + return node.release_nonnull(); + } + + // 5. Return root. + // NOTE: Because our root is immutable, we have to return a new node with the modified children. + return ProductCalculationNode::create(move(children)); + } + + // AD-HOC: Math-function nodes that cannot be fully simplified will reach here. + // Spec bug: https://github.com/w3c/csswg-drafts/issues/11572 + return root; +} + } diff --git a/Libraries/LibWeb/CSS/StyleValues/CalculatedStyleValue.h b/Libraries/LibWeb/CSS/StyleValues/CalculatedStyleValue.h index dba48e59df7..84cef7b6be3 100644 --- a/Libraries/LibWeb/CSS/StyleValues/CalculatedStyleValue.h +++ b/Libraries/LibWeb/CSS/StyleValues/CalculatedStyleValue.h @@ -241,6 +241,9 @@ public: Optional const& numeric_type() const { return m_numeric_type; } virtual bool contains_percentage() const = 0; virtual CalculatedStyleValue::CalculationResult resolve(CalculationResolutionContext const&) const = 0; + virtual NonnullRefPtr with_simplified_children(CalculationContext const&, CalculationResolutionContext const&) const = 0; + // Step 4 of simplify_a_calculation_tree(). Only valid for math-function nodes. + virtual Optional run_operation_if_possible(CalculationContext const&, CalculationResolutionContext const&) const { VERIFY_NOT_REACHED(); } virtual void dump(StringBuilder&, int indent) const = 0; virtual bool equals(CalculationNode const&) const = 0; @@ -260,7 +263,11 @@ public: virtual String to_string() const override; virtual bool contains_percentage() const override; + bool is_in_canonical_unit() const; virtual CalculatedStyleValue::CalculationResult resolve(CalculationResolutionContext const&) const override; + virtual NonnullRefPtr with_simplified_children(CalculationContext const&, CalculationResolutionContext const&) const override { return *this; } + + NumericValue const& value() const { return m_value; } virtual void dump(StringBuilder&, int indent) const override; virtual bool equals(CalculationNode const&) const override; @@ -278,6 +285,9 @@ public: virtual String to_string() const override; virtual bool contains_percentage() const override; virtual CalculatedStyleValue::CalculationResult resolve(CalculationResolutionContext const&) const override; + virtual NonnullRefPtr with_simplified_children(CalculationContext const&, CalculationResolutionContext const&) const override; + + Vector> const& children() const { return m_values; } virtual void dump(StringBuilder&, int indent) const override; virtual bool equals(CalculationNode const&) const override; @@ -295,6 +305,9 @@ public: virtual String to_string() const override; virtual bool contains_percentage() const override; virtual CalculatedStyleValue::CalculationResult resolve(CalculationResolutionContext const&) const override; + virtual NonnullRefPtr with_simplified_children(CalculationContext const&, CalculationResolutionContext const&) const override; + + Vector> const& children() const { return m_values; } virtual void dump(StringBuilder&, int indent) const override; virtual bool equals(CalculationNode const&) const override; @@ -312,6 +325,9 @@ public: virtual String to_string() const override; virtual bool contains_percentage() const override; virtual CalculatedStyleValue::CalculationResult resolve(CalculationResolutionContext const&) const override; + virtual NonnullRefPtr with_simplified_children(CalculationContext const&, CalculationResolutionContext const&) const override; + + CalculationNode const& child() const { return m_value; } virtual void dump(StringBuilder&, int indent) const override; virtual bool equals(CalculationNode const&) const override; @@ -329,6 +345,9 @@ public: virtual String to_string() const override; virtual bool contains_percentage() const override; virtual CalculatedStyleValue::CalculationResult resolve(CalculationResolutionContext const&) const override; + virtual NonnullRefPtr with_simplified_children(CalculationContext const&, CalculationResolutionContext const&) const override; + + CalculationNode const& child() const { return m_value; } virtual void dump(StringBuilder&, int indent) const override; virtual bool equals(CalculationNode const&) const override; @@ -346,6 +365,10 @@ public: virtual String to_string() const override; virtual bool contains_percentage() const override; virtual CalculatedStyleValue::CalculationResult resolve(CalculationResolutionContext const&) 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; + + Vector> const& children() const { return m_values; } virtual void dump(StringBuilder&, int indent) const override; virtual bool equals(CalculationNode const&) const override; @@ -363,6 +386,10 @@ public: virtual String to_string() const override; virtual bool contains_percentage() const override; virtual CalculatedStyleValue::CalculationResult resolve(CalculationResolutionContext const&) 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; + + Vector> const& children() const { return m_values; } virtual void dump(StringBuilder&, int indent) const override; virtual bool equals(CalculationNode const&) const override; @@ -380,6 +407,8 @@ public: virtual String to_string() const override; virtual bool contains_percentage() const override; virtual CalculatedStyleValue::CalculationResult resolve(CalculationResolutionContext const&) 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 void dump(StringBuilder&, int indent) const override; virtual bool equals(CalculationNode const&) const override; @@ -399,6 +428,8 @@ public: virtual String to_string() const override; virtual bool contains_percentage() const override; virtual CalculatedStyleValue::CalculationResult resolve(CalculationResolutionContext const&) 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 void dump(StringBuilder&, int indent) const override; virtual bool equals(CalculationNode const&) const override; @@ -416,6 +447,8 @@ public: virtual String to_string() const override; virtual bool contains_percentage() const override; virtual CalculatedStyleValue::CalculationResult resolve(CalculationResolutionContext const&) 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 void dump(StringBuilder&, int indent) const override; virtual bool equals(CalculationNode const&) const override; @@ -433,6 +466,7 @@ public: virtual String to_string() const override; virtual bool contains_percentage() const override { return false; } virtual CalculatedStyleValue::CalculationResult resolve(CalculationResolutionContext const&) const override; + virtual NonnullRefPtr with_simplified_children(CalculationContext const&, CalculationResolutionContext const&) const override { return *this; } virtual void dump(StringBuilder&, int indent) const override; virtual bool equals(CalculationNode const&) const override; @@ -450,6 +484,8 @@ public: virtual String to_string() const override; virtual bool contains_percentage() const override; virtual CalculatedStyleValue::CalculationResult resolve(CalculationResolutionContext const&) 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 void dump(StringBuilder&, int indent) const override; virtual bool equals(CalculationNode const&) const override; @@ -467,6 +503,8 @@ public: virtual String to_string() const override; virtual bool contains_percentage() const override; virtual CalculatedStyleValue::CalculationResult resolve(CalculationResolutionContext const&) 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 void dump(StringBuilder&, int indent) const override; virtual bool equals(CalculationNode const&) const override; @@ -484,6 +522,8 @@ public: virtual String to_string() const override; virtual bool contains_percentage() const override; virtual CalculatedStyleValue::CalculationResult resolve(CalculationResolutionContext const&) 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 void dump(StringBuilder&, int indent) const override; virtual bool equals(CalculationNode const&) const override; @@ -501,6 +541,8 @@ public: virtual String to_string() const override; virtual bool contains_percentage() const override; virtual CalculatedStyleValue::CalculationResult resolve(CalculationResolutionContext const&) 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 void dump(StringBuilder&, int indent) const override; virtual bool equals(CalculationNode const&) const override; @@ -518,6 +560,8 @@ public: virtual String to_string() const override; virtual bool contains_percentage() const override; virtual CalculatedStyleValue::CalculationResult resolve(CalculationResolutionContext const&) 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 void dump(StringBuilder&, int indent) const override; virtual bool equals(CalculationNode const&) const override; @@ -535,6 +579,8 @@ public: virtual String to_string() const override; virtual bool contains_percentage() const override; virtual CalculatedStyleValue::CalculationResult resolve(CalculationResolutionContext const&) 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 void dump(StringBuilder&, int indent) const override; virtual bool equals(CalculationNode const&) const override; @@ -552,6 +598,8 @@ public: virtual String to_string() const override; virtual bool contains_percentage() const override; virtual CalculatedStyleValue::CalculationResult resolve(CalculationResolutionContext const&) 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 void dump(StringBuilder&, int indent) const override; virtual bool equals(CalculationNode const&) const override; @@ -570,6 +618,8 @@ public: virtual String to_string() const override; virtual bool contains_percentage() const override { return false; } virtual CalculatedStyleValue::CalculationResult resolve(CalculationResolutionContext const&) 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 void dump(StringBuilder&, int indent) const override; virtual bool equals(CalculationNode const&) const override; @@ -588,6 +638,8 @@ public: virtual String to_string() const override; virtual bool contains_percentage() const override { return false; } virtual CalculatedStyleValue::CalculationResult resolve(CalculationResolutionContext const&) 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 void dump(StringBuilder&, int indent) const override; virtual bool equals(CalculationNode const&) const override; @@ -605,6 +657,10 @@ public: virtual String to_string() const override; virtual bool contains_percentage() const override; virtual CalculatedStyleValue::CalculationResult resolve(CalculationResolutionContext const&) 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; + + Vector> const& children() const { return m_values; } virtual void dump(StringBuilder&, int indent) const override; virtual bool equals(CalculationNode const&) const override; @@ -622,6 +678,8 @@ public: virtual String to_string() const override; virtual bool contains_percentage() const override { return false; } virtual CalculatedStyleValue::CalculationResult resolve(CalculationResolutionContext const&) 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 void dump(StringBuilder&, int indent) const override; virtual bool equals(CalculationNode const&) const override; @@ -640,6 +698,8 @@ public: virtual String to_string() const override; virtual bool contains_percentage() const override { return false; } virtual CalculatedStyleValue::CalculationResult resolve(CalculationResolutionContext const&) 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 void dump(StringBuilder&, int indent) const override; virtual bool equals(CalculationNode const&) const override; @@ -657,6 +717,8 @@ public: virtual String to_string() const override; virtual bool contains_percentage() const override; virtual CalculatedStyleValue::CalculationResult resolve(CalculationResolutionContext const&) 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 void dump(StringBuilder&, int indent) const override; virtual bool equals(CalculationNode const&) const override; @@ -676,6 +738,8 @@ public: virtual String to_string() const override; virtual bool contains_percentage() const override; virtual CalculatedStyleValue::CalculationResult resolve(CalculationResolutionContext const&) 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 void dump(StringBuilder&, int indent) const override; virtual bool equals(CalculationNode const&) const override; @@ -694,6 +758,8 @@ public: virtual String to_string() const override; virtual bool contains_percentage() const override; virtual CalculatedStyleValue::CalculationResult resolve(CalculationResolutionContext const&) 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 void dump(StringBuilder&, int indent) const override; virtual bool equals(CalculationNode const&) const override; @@ -704,4 +770,7 @@ private: NonnullRefPtr m_y; }; +// https://drafts.csswg.org/css-values-4/#calc-simplification +NonnullRefPtr simplify_a_calculation_tree(CalculationNode const& root, CalculationContext const& context, CalculationResolutionContext const& resolution_context); + }