LibWeb: Avoid premature creation of CSSPixels in calc simplification
Some checks are pending
CI / macOS, arm64, Sanitizer, Clang (push) Waiting to run
CI / Linux, x86_64, Fuzzers, Clang (push) Waiting to run
CI / Linux, x86_64, Sanitizer, GNU (push) Waiting to run
CI / Linux, x86_64, Sanitizer, Clang (push) Waiting to run
Package the js repl as a binary artifact / Linux, arm64 (push) Waiting to run
Package the js repl as a binary artifact / macOS, arm64 (push) Waiting to run
Package the js repl as a binary artifact / Linux, x86_64 (push) Waiting to run
Run test262 and test-wasm / run_and_update_results (push) Waiting to run
Lint Code / lint (push) Waiting to run
Label PRs with merge conflicts / auto-labeler (push) Waiting to run
Push notes / build (push) Waiting to run

Previously we were converting lengths to CSSPixels values when we didn't
need to, this had a couple of effects in that:
 - We rounded to CSSPixel resolution prematurely (sometimes giving
   incorrect results)
 - We converted NaN to 0 when we shouldn't have.

We now avoid prematurely converting lengths to CSSPixels values in two
places:
 - `CalculationResult::from_value`
 - `CalculatedStyleValue::resolve_length_deprecated` (the new method
   already avoided rounding).

Gains us 16 WPT tests.
This commit is contained in:
Callum Law 2025-07-31 21:07:03 +12:00 committed by Sam Atkins
commit a44e28fd56
Notes: github-actions[bot] 2025-08-06 14:01:09 +00:00
4 changed files with 74 additions and 17 deletions

View file

@ -197,24 +197,30 @@ public:
}
ALWAYS_INLINE CSSPixels absolute_length_to_px() const
{
return CSSPixels::nearest_value_for(absolute_length_to_px_without_rounding());
}
ALWAYS_INLINE double absolute_length_to_px_without_rounding() const
{
constexpr double inch_pixels = 96.0;
constexpr double centimeter_pixels = (inch_pixels / 2.54);
switch (m_type) {
case Type::Cm:
return CSSPixels::nearest_value_for(m_value * centimeter_pixels); // 1cm = 96px/2.54
return m_value * centimeter_pixels; // 1cm = 96px/2.54
case Type::In:
return CSSPixels::nearest_value_for(m_value * inch_pixels); // 1in = 2.54 cm = 96px
return m_value * inch_pixels; // 1in = 2.54 cm = 96px
case Type::Px:
return CSSPixels::nearest_value_for(m_value); // 1px = 1/96th of 1in
return m_value; // 1px = 1/96th of 1in
case Type::Pt:
return CSSPixels::nearest_value_for(m_value * ((1.0 / 72.0) * inch_pixels)); // 1pt = 1/72th of 1in
return m_value * ((1.0 / 72.0) * inch_pixels); // 1pt = 1/72th of 1in
case Type::Pc:
return CSSPixels::nearest_value_for(m_value * ((1.0 / 6.0) * inch_pixels)); // 1pc = 1/6th of 1in
return m_value * ((1.0 / 6.0) * inch_pixels); // 1pc = 1/6th of 1in
case Type::Mm:
return CSSPixels::nearest_value_for(m_value * ((1.0 / 10.0) * centimeter_pixels)); // 1mm = 1/10th of 1cm
return m_value * ((1.0 / 10.0) * centimeter_pixels); // 1mm = 1/10th of 1cm
case Type::Q:
return CSSPixels::nearest_value_for(m_value * ((1.0 / 40.0) * centimeter_pixels)); // 1Q = 1/40th of 1cm
return m_value * ((1.0 / 40.0) * centimeter_pixels); // 1Q = 1/40th of 1cm
default:
VERIFY_NOT_REACHED();
}

View file

@ -2600,7 +2600,7 @@ CalculatedStyleValue::CalculationResult CalculatedStyleValue::CalculationResult:
return 0.0;
if (length.is_absolute())
return length.absolute_length_to_px().to_double();
return length.absolute_length_to_px_without_rounding();
// If we don't have a context, we cant resolve the length, so return NAN
if (!context.length_resolution_context.has_value()) {
@ -2751,7 +2751,7 @@ Optional<Length> CalculatedStyleValue::resolve_length_deprecated(CalculationReso
{
auto result = m_calculation->resolve(context);
if (result.type().has_value() && result.type()->matches_length(m_context.percentages_resolve_as))
return Length::make_px(CSSPixels { result.value() });
return Length::make_px(result.value());
return {};
}

View file

@ -0,0 +1,51 @@
Harness status: OK
Found 45 tests
33 Pass
12 Fail
Pass Property width value 'calc(NaN * 1px)'
Pass Property width value 'calc(NaN * 1%)'
Pass Property width value 'calc(infinity * 1px)'
Pass Property width value 'calc(infinity * 1%)'
Pass Property width value 'calc(infinity * 1cm)'
Pass Property width value 'calc(NaN * 1rem)'
Pass Property width value 'calc(10.135262721212548pc - 199pt / NaN)'
Pass Property width value 'max(15px, NaN * 1px)'
Pass Property width value 'max(NaN * 1px, 15px)'
Pass Property width value 'max(-15px, NaN * 1px)'
Pass Property width value 'max(NaN * 1px, -15px)'
Pass Property width value 'min(15px, NaN * 1px)'
Pass Property width value 'min(NaN * 1px, 15px)'
Pass Property width value 'min(-15px, NaN * 1px)'
Pass Property width value 'min(NaN * 1px, -15px)'
Pass Property width value 'calc(infinity * 1px - infinity * 1%)'
Pass Property width value 'calc(infinity * 1px + infinity * 1%)'
Pass Property width value 'calc(min(NaN * 1px, infinity * 1px) + max(infinity * 1px, -infinity * 1px))'
Pass Property width value 'calc(infinity * 1px - max(infinity * 1%, 0%))'
Pass Property width value 'calc(max(infinity * 1px, 10px))'
Pass Property margin-left value 'calc(-infinity * 1px)'
Pass Property margin-left value 'calc(min(1px, -infinity * 1%))'
Pass Property margin-left value 'calc(-infinity * 1%)'
Pass Property margin-left value 'calc(max(10000px, 0px) + min(-infinity * 1px, infinity * 1px))'
Pass Property margin-left value 'calc(-infinity * 1px - infinity * 1px)'
Pass Property margin-left value 'calc(min(-infinity * 1px, 10px))'
Pass Property animation-duration value 'calc(NaN * 1s)'
Pass Property animation-duration value 'calc(infinity * 1s)'
Pass Property animation-duration value 'calc(1 / 0 * 1s)'
Pass Property animation-duration value 'calc(max(infinity * 1s, 10s)'
Pass Property transition-delay value 'calc(-infinity* 1s)'
Pass Property transition-delay value 'calc(max(10000s, 0s) + min(-infinity * 1s, infinity * 1s))'
Pass Property transition-delay value 'calc(min(-infinity * 1s, 10s))'
Fail Property rotate(calc(infinity * 1deg)) value expected same with rotate(0deg) in +/-0.0001
Fail Property rotate(calc(-infinity * 1deg)) value expected same with rotate(0deg) in +/-0.0001
Fail Property rotate(calc(NaN * 1deg)) value expected same with rotate(0deg) in +/-0.0001
Fail Property rotate(calc(infinity * 1turn)) value expected same with rotate(0deg) in +/-0.0001
Fail Property rotate(calc(-infinity * 1turn)) value expected same with rotate(0deg) in +/-0.0001
Fail Property rotate(calc(NaN * 1turn)) value expected same with rotate(0deg) in +/-0.0001
Fail Property rotate(calc(infinity * 1rad)) value expected same with rotate(0deg) in +/-0.0001
Fail Property rotate(calc(-infinity * 1rad)) value expected same with rotate(0deg) in +/-0.0001
Fail Property rotate(calc(NaN * 1rad)) value expected same with rotate(0deg) in +/-0.0001
Fail Property rotate(calc(infinity * 1grad)) value expected same with rotate(0deg) in +/-0.0001
Fail Property rotate(calc(-infinity * 1grad)) value expected same with rotate(0deg) in +/-0.0001
Fail Property rotate(calc(NaN * 1grad)) value expected same with rotate(0deg) in +/-0.0001

View file

@ -2,8 +2,8 @@ Harness status: OK
Found 41 tests
29 Pass
12 Fail
35 Pass
6 Fail
Pass 'calc(1px * NaN)' as a specified value should serialize as 'calc(NaN * 1px)'.
Pass 'calc(1% * NaN)' as a specified value should serialize as 'calc(NaN * 1%)'.
Pass 'calc(1in * NaN)' as a specified value should serialize as 'calc(NaN * 1px)'.
@ -13,7 +13,7 @@ Pass 'calc(1q * NaN)' as a specified value should serialize as 'calc(NaN * 1px)'
Pass 'calc(1pt * NaN)' as a specified value should serialize as 'calc(NaN * 1px)'.
Pass 'calc(1pc * NaN)' as a specified value should serialize as 'calc(NaN * 1px)'.
Pass 'calc(1px * nan)' as a specified value should serialize as 'calc(NaN * 1px)'.
Fail 'calc(1px * infinity / infinity)' as a specified value should serialize as 'calc(NaN * 1px)'.
Pass 'calc(1px * infinity / infinity)' as a specified value should serialize as 'calc(NaN * 1px)'.
Pass 'calc(1px * 0 * infinity)' as a specified value should serialize as 'calc(NaN * 1px)'.
Pass 'calc(1px * (infinity + -infinity))' as a specified value should serialize as 'calc(NaN * 1px)'.
Pass 'calc(1px * (-infinity + infinity))' as a specified value should serialize as 'calc(NaN * 1px)'.
@ -28,10 +28,10 @@ Pass 'calc(1px * (-infinity + -infinity))' as a specified value should serialize
Pass 'calc(1px * 1/infinity)' as a specified value should serialize as 'calc(0px)'.
Pass 'calc(1px * infinity * infinity)' as a specified value should serialize as 'calc(infinity * 1px)'.
Pass 'calc(1px * -infinity * -infinity)' as a specified value should serialize as 'calc(infinity * 1px)'.
Fail 'calc(1 * max(INFinity*3px, 0px))' as a specified value should serialize as 'calc(infinity * 1px)'.
Pass 'calc(1 * max(INFinity*3px, 0px))' as a specified value should serialize as 'calc(infinity * 1px)'.
Pass 'calc(1 * min(inFInity*4px, 0px))' as a specified value should serialize as 'calc(0px)'.
Fail 'calc(1 * max(nAn*2px, 0px))' as a specified value should serialize as 'calc(NaN * 1px)'.
Fail 'calc(1 * min(nan*3px, 0px))' as a specified value should serialize as 'calc(NaN * 1px)'.
Pass 'calc(1 * max(nAn*2px, 0px))' as a specified value should serialize as 'calc(NaN * 1px)'.
Pass 'calc(1 * min(nan*3px, 0px))' as a specified value should serialize as 'calc(NaN * 1px)'.
Pass 'calc(1 * clamp(-INFINITY*20px, 0px, infiniTY*10px))' as a specified value should serialize as 'calc(0px)'.
Pass 'calc(1px * max(NaN, min(0,10)))' as a specified value should serialize as 'calc(NaN * 1px)'.
Pass 'calc(1px * clamp(NaN, 0, 10))' as a specified value should serialize as 'calc(NaN * 1px)'.
@ -41,7 +41,7 @@ Fail 'calc(1px * max(0, min(NaN, 10)))' as a specified value should serialize as
Fail 'calc(1px * clamp(0, NaN, 10))' as a specified value should serialize as 'calc(NaN * 1px)'.
Pass 'calc(1px * clamp(-Infinity, 0, infinity))' as a specified value should serialize as 'calc(0px)'.
Pass 'calc(1px * clamp(-inFinity, infinity, 10))' as a specified value should serialize as 'calc(10px)'.
Fail 'calc(1 * min(NaN * 1pt, NaN * 1cm))' as a specified value should serialize as 'calc(NaN * 1px)'.
Fail 'calc(1 * max(NaN * 1cm, NaN * 2Q))' as a specified value should serialize as 'calc(NaN * 1px)'.
Pass 'calc(1 * min(NaN * 1pt, NaN * 1cm))' as a specified value should serialize as 'calc(NaN * 1px)'.
Pass 'calc(1 * max(NaN * 1cm, NaN * 2Q))' as a specified value should serialize as 'calc(NaN * 1px)'.
Fail 'calc(1 * min(NaN * 2px, NaN * 4em))' as a specified value should serialize as 'calc(NaN * 1px)'.
Fail 'calc(1 * clamp(NaN * 2em, NaN * 4px, NaN * 8pt))' as a specified value should serialize as 'clamp(NaN * 1em, NaN * 1px, NaN * 1px)'.