Previously, FontFace objects created via the JS and added to
`document.fonts` were stored in the FontFaceSet but never participated
in font matching during style resolution. We now store both
CSS-connected and JS-created font faces in a unified map on
`FontComputer`, keyed by family name, and include them all as
candidates in the font matching algorithm.
Blink, WebKit, and Gecko round the border-box geometry that feeds
offsetWidth and offsetHeight instead of truncating it. Do the same
in HTMLElement so these CSSOM View APIs match interop behavior for
fractional sizes.
This also updates the two local text expectations that changed
because they observe the rounded values.
When a `@keyframes` rule contains `animation-timing-function` with a
`var()`, we cannot eagerly resolve it to an `EasingFunction` at rule
cache build time because there is no element context available. We now
store the unresolved `StyleValue` and defer resolution to
`collect_animation_into()`, where the animated element's custom
properties can be used to substitute the variable. Previously, an
`animation-timing-function` with a `var()` in a `@keyframe` would cause
a crash.
`@function` descriptors are the only ones that support ASFs, while most
descriptors enforce this through their syntaxes implicitly disallowing
ASFs, this wasn't the case for `@property/initial-value`.
We now explictly disallow ASFs unless they are marked as allowed within
`Descriptors.json`.
Everywhere we use this expects us to parse the whole value, either
because we are parsing the value of a declaration (in which case there
will be no semicolons), or because it is called from a JS setter which
takes whole values and semicolons make the value invalid.
Previously we would just ignore everything after a semicolon.
This also allows us to avoid creating a new `Vector` and copying all the
component values
When a shorthand like `background` containing `var()` is used in
a `::selection` rule, the shorthand was filtered out by the pseudo-
element property whitelist before variable resolution could occur.
This left PendingSubstitutionStyleValue longhands unresolved,
causing either a crash or incorrect computed values.
Allow unresolved shorthands to bypass the pseudo-element filter so
variable resolution can proceed. After resolution and expansion
into longhands, filter out any that the pseudo-element does not
support.
Fixes#8625.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The output format for this was confusing. When "FAIL" becomes "PASS" the
natural assumption is that's good. So, make it always output pass for
correct results, and fail for incorrect. Also, replace the
supposed-to-fail pseudo-element name with one that will never be
supported, instead of a webkit one that we did end up adding support
for! 😅
Added a couple of env() cases which will pass with the following commit.
For some text decorations (e.g. underline) we were using the line's top
edge as the Y-coordinate to draw the line at, which combined with the
line's thickness meant that it was positioned too high up.
Correct this by calculating the line's center Y position.
Currently this only applies to the `@property` `syntax` descriptor.
As with custom properties in the previous commit we assumed that any
consumed values were valid but that's not the case.
This requires us to front load computation of writing-mode and direction
before we encounter any logical aliases or their physical counterparts
so that we can create a mapping context.
Doing this at compute rather than cascade time achieves a few things:
1) Brings us into line with the spec
2) Avoids the double cascade that was previously required to compute
mapping contexts
3) We now compute values of logical aliases, while
`style_value_for_computed_property` maps logical aliases to their
physical counterparts, this didn't account for all cases (i.e. if
there was no layout node, Typed OM, etc).
4) Removes a hurdle to moving other upstream processes (i.e. arbitrary
substitution function resolution, custom property computation) to
compute time as the spec requires.
The hover invalidation code only tried matching ::before/::after
selectors when has_pseudo_element() returned true, which requires an
existing layout node. A pseudo-element that doesn't exist yet (because
its content is only set by a hover rule) has no layout node, so the
match was skipped and hovering never triggered a style recompute.
Always try ::before/::after selectors during hover invalidation.
We no longer try to resolve calculated values at parse time which means
we support relative lengths.
We now clamp negative values rather than rejecting them at parse time.
Parsing has been inlined into `parse_ratio_value` and `parse_ratio` has
been removed since `parse_ratio_value` was the only caller
Avoid comparing exact matrix3d values which contain floating point
noise that could differ across platforms. Instead, check whether the
transform is or isn't the identity matrix.
Per the CSS Transforms spec, when interpolating rotate3d() functions
with equal normalized direction vectors (or when one angle is zero),
the rotation angle should be interpolated numerically rather than
using quaternion slerp.
Previously we always used quaternion slerp, which cannot represent
rotations beyond 360 degrees. This meant that animating from
rotateY(0deg) to rotateY(3600deg) produced no visual animation, since
both quaternions are identical (3600 mod 360 = 0).
Now we detect when axes match and interpolate the angle directly,
correctly preserving multi-turn rotations. This fixes 168 WPT tests.
When interpolating between rotateY(0deg) and rotateY(3600deg), the
angle should be interpolated numerically. Currently we use quaternion
slerp which sees these as identical rotations (3600 mod 360 = 0),
producing no visual animation. A following commit will fix this.
Per the CSS Animations spec, the animation-timing-function property
describes how the animation progresses between each pair of keyframes,
not as an overall effect-level timing function.
Previously we set it as the effect-level timing function on the
AnimationEffect, which caused easing to be applied to the global
animation progress. This made animations with multiple keyframes
"pause" at the start and end of the full animation cycle instead of
easing smoothly between each pair of keyframes.
Now we:
- Store per-keyframe easing in ResolvedKeyFrame from @keyframes rules
- Store the default easing on CSSAnimation instead of on the effect
- Apply per-keyframe easing to the interval progress during
interpolation, falling back to the CSS animation's default easing
- Also store per-keyframe easing from JS-created KeyframeEffects to
avoid incorrectly applying CSS default easing to replaced effects
The CSS spec says animation-timing-function is applied per keyframe
interval, not as an overall effect-level timing function. Currently we
apply it globally, causing wrong intermediate values and ignoring
per-keyframe animation-timing-function declarations in @keyframes
rules. A following commit will fix this.
When processing CSS animations, we were only looking up @keyframes
rules from the document-level rule cache, passing nullptr for the
shadow root parameter. This meant that @keyframes defined inside
shadow DOM <style> elements were never found, and animations
referencing them would silently have no keyframes.
Fix this by first checking the element's containing shadow root for
@keyframes rules, then falling back to the document-level rules.
When @keyframes rules are defined in a <style> element inside a shadow
root, animations referencing them should work. Currently the animation
is created but has no keyframes, producing "none" for animated
transform values. A following commit will fix this.
Per the CSS Transforms spec, when one of the matrices for interpolation
is non-invertible (i.e. cannot be decomposed), the animation must fall
back to discrete interpolation.
Previously we would silently drop the transform, producing "none". Now
we correctly snap between the from/to values at the 50% progress mark,
matching the behavior required by the spec and other browsers.
Also propagate to_matrix() errors instead of silently using a partial
result, and use the previously-unused AllowDiscrete parameter.
When a transform animation involves a non-invertible matrix (e.g. a
matrix3d with zero Z-scale), the spec requires falling back to discrete
interpolation. Currently we drop the transform entirely, producing
"none" at all progress values. A following commit will fix this.
When a var() fallback value contained trailing whitespace (e.g.
`var(--foo, flex )`), parse_display_value() miscounted the tokens.
The remaining_token_count() check included whitespace tokens, routing
single-keyword values like "flex" to the multi-component path. The
multi-component parser then failed when encountering the trailing
whitespace token.
Fix this by counting only non-whitespace tokens for the single vs
multi-component routing decision, and by skipping whitespace in the
multi-component parsing loop.
This test shows that var() fallback values with trailing whitespace
(e.g. `var(--foo, flex )`) fail to parse correctly for the display
property, falling back to initial values instead.
When a CSS animation keyframe uses var() referencing a nonexistent or
invalid custom property, variable substitution produces a
guaranteed-invalid value. The animation keyframe processing code did not
handle this case, allowing the value to reach compute_opacity() (and
similar functions) which would hit VERIFY_NOT_REACHED().
Fix this by skipping guaranteed-invalid values in
compute_keyframe_values, matching how the regular cascading code treats
them.
This fixes a crash on chess.com.
See previous commit for details
We now support parsing of `display-p3-linear` (although it just falls
back to using Oklab since Skia doesn't support it)
Replace per-frame heap-allocated RefCounted ScrollFrame objects with a
single contiguous Vector<ScrollFrame> inside ScrollState. All frames for
a viewport are now stored in one allocation, using type-safe
ScrollFrameIndex instead of RefPtr pointers.
This reduces allocation churn, improves cache locality, and moves
parent-chain traversal (cumulative offset, nearest scrolling ancestor)
into ScrollState — similar to how visual context nodes were recently
consolidated into AccumulatedVisualContextTree.
Replace flat InvalidationSet with recursive InvalidationPlan trees
that preserve selector combinator structure. Previously, selectors
with sibling combinators (+ and ~) fell back to whole-subtree
invalidation. Now the StyleInvalidator walks the DOM following
combinator-specific rules, so ".a + .b" only invalidates the
adjacent sibling matching ".b" rather than the entire subtree.
Plans are compiled at stylesheet parse time by walking selector
compounds right-to-left. For ".a .b + .c":
```
[.c]: plan = { invalidate_self }
register: "c" → plan
[.b]: wrap("+", righthand)
plan = { sibling_rules: [match ".c", adjacent, {self}] }
register: "b" → plan
[.a]: wrap(" ", righthand)
plan = { descendant_rules: [match ".b", <sibling plan>] }
register: "a" → plan
```
Changing class "a" produces a plan that walks descendants for ".b",
checks ".b"'s adjacent sibling for ".c", and invalidates only that
element.
The style of an element depends on it's navigable's viewport size which
in turn depends on the navigable's container's style - so if requires a
style update then so does the original element.
When CSSRule.cssText is accessed, shorthands are recomposed from
individual longhand declarations. For coordinating-list shorthands, the
`serialize()` function always assumed that each sub-property was a
value list. This caused a crash for longhands containing `var()`. We
now fall back to serializing properties individually if any sub
property contains `var()`. This matches the behavior of other engines.
The selector matching code bypassed the pseudo-element type check for
`::part()` selectors to support compound selectors like
`::part(foo)::before`. This caused bare ::part() declarations to leak
into unrelated pseudo-elements like ::selection.
Fix this by finding any additional pseudo-element beyond ::part() in the
selector and verifying it matches the target.
The grammar groups this component together meaning that all
sub-components must occur together i.e.
`ordinal slashed-zero small-caps` is valid but
`ordinal small-caps slashed-zero` is not.
We also reuse the logic for parsing from the longhand
`font-variant-numeric` property for simplicity.
The grammar groups this component together meaning that all
sub-components must occur together i.e.
`common-ligatures no-contextual small-caps` is valid but
`common-ligatures small-caps no-contextual` is not.
We also reuse the logic for parsing from the longhand
`font-variant-ligatures` property for simplicity.