Commit Graph

290 Commits

Author SHA1 Message Date
Andreas Kling
c9cac66677 Tests: Expand style invalidation coverage
Add broader style-invalidation regression coverage for selector shapes
that are easy to mis-model when invalidation data is flattened into
feature sets. Cover selector-list alternatives, nested pseudo-class
arguments, pseudo-element arguments, nth-child selector lists,
quirks-mode class matching, mixed-case ancestor filters, duplicate
invalidation rule merging, and concrete :has() feature filtering.

Keep the expectations on the unoptimized baseline so follow-up
optimization commits can show the counter progressions separately.
These tests dump the normal invalidation counters instead of asserting
specific counter values in script.
2026-04-29 15:47:23 +02:00
Callum Law
9a3f2b23a1 LibWeb: Add method to parse specific CSS keyword
This revealed an issue with `@counter-style/range` where parsing could
consume and ignore invalid keywords
2026-04-29 11:42:57 +01:00
Andreas Kling
fc931c93a9 Tests: Cover abspos CB for inline-relative with block descendant
An `inset: 0` abspos child of an inline-relative with block-level
descendants should size to the inline's full extent across the
"before"/"middle"/"after" wrappers. The expected output records the
current buggy state (most cases collapse to the viewport); the next
commit fixes it.
2026-04-29 04:54:11 +02:00
Andreas Kling
5e2474e8c9 Tests: Cover sibling structural invalidation fanout
Pin the recompute counters (and shadow-DOM correctness) when a
structural mutation flips a sibling's match for :first-child,
:last-child, :nth-child, :nth-of-type, or a + / ~ combinator. The
affected sibling's entire subtree is marked dirty today, so an 8-leaf
sibling produces elementStyleRecomputations=11. Shadow-host cases pin
that inherited-style propagation reaches light DOM, light-DOM-hosted
shadow roots, and nested shadow DOM under the affected sibling.

The next commit replaces the entire-subtree mark with a root-only mark
when no descendant selector ties matching to the sibling's position,
and rebaselines the counter expectations.
2026-04-29 04:53:14 +02:00
Andreas Kling
3b098c08ea Tests: Cover media-query first-evaluation invalidation behavior
Add focused coverage for the counters produced when a stylesheet's media
queries are evaluated for the first time. Two paths are exercised:

- document.adoptedStyleSheets, where the sheet is added without an
  eager evaluate_media_queries call. The next update_style currently
  flips m_did_match from "no recorded result" to a recorded one and
  fires MediaQueryChangedMatchState on top of the AdoptedStyleSheetsList
  invalidation, so fullStyleInvalidations bumps twice.

- <style> element add via StyleSheetList::add_sheet, which evaluates
  media queries eagerly and absorbs the first-eval flip. These tests
  pin that behavior so future changes don't regress it.

Cases vary the outer MediaList (empty, all, screen, print, width-based
matching and not), the rule selector (matching, non-matching), inner
@media rules (matching, non-matching, mixed, nested), and @supports
wrapping inner @media. The expectations record behavior before the
optimization, so subsequent commits can show counter progressions.
2026-04-28 19:06:29 +02:00
Andreas Kling
bacfbb4d97 Tests: Cover structural style invalidation edge cases
Add focused coverage for style invalidation matrix behavior.
Cover concrete invalidation metadata collection, :defined triggers,
same-parent moves, :has() mutation roots, and feature filtering.

Also cover dynamic and unprobeable pseudo-classes inside :has().
These include mixed-metadata cases where another :has() selector in the
same scope records concrete metadata.

Add SVG and MathML case-sensitive name coverage for :has() filters.

The expectations record behavior before the optimization, so the next
commit can show counter progressions clearly.
2026-04-28 15:34:49 +02:00
Andreas Kling
caad205467 LibWeb: Share singleton constructed stylesheet rule caches
Share the style cache for shadow roots whose only active author sheet is
the same constructed stylesheet. Matching already carries the effective
shadow root separately, so the cache can be reused while selectors such
as :host and ::slotted() still evaluate against each consuming shadow
root.

Keep the optimization conservative by falling back to the existing
per-scope cache whenever the shadow root has multiple active sheets, a
non-constructed sheet, or a page user stylesheet. Drop the shared cache
when the stylesheet rules or media query match state change.

Add coverage for two shadow roots adopting the same constructed sheet,
including :host, ::slotted(), and replaceSync() invalidation.
2026-04-28 13:07:52 +02:00
Andreas Kling
b8e5f07eed LibWeb: Keep rule caches for CSSOM declaration changes
CSSOM declaration mutations on style rules and nested declarations
do not change selector buckets, layer ordering, or keyframe sets stored
in rule caches. Keep those caches valid and only invalidate affected
styles, while leaving keyframe declaration mutations on the existing
cache-invalidating path.

Add coverage showing a CSSOM style declaration mutation is observed
through an already-built rule cache.
2026-04-28 09:49:50 +02:00
Andreas Kling
118802b3f0 LibWeb: Scope media rule cache invalidation
Invalidate only the style scope whose media rules changed instead
of throwing away every shadow root rule cache whenever any active
stylesheet changes media query match state. Shadow-root stylesheet
changes still dirty the host side because :host and ::slotted
selectors can affect nodes outside the shadow tree.

When scoped invalidation leaves dirty descendants in a shadow root,
preserve the host ancestor chain so the document style update walk
reaches them before forced layout.

Add coverage that a matching media rule introduced in one shadow tree
does not broadly invalidate a page full of unrelated shadow roots,
and that a dirty shadow root is updated before layout is forced.
2026-04-28 09:49:50 +02:00
Luke Wilde
b3ecb3c6da LibWeb/CSS: Implement stroke-dasharray interpolation 2026-04-27 19:06:01 +02:00
Tim Ledbetter
6bb037aec7 LibWeb: Skip backward sibling invalidation when no child needs it
We now track when a parent has a child affected by a backward structural
pseudo-class. These are selectors whose match result for an element can
depend on siblings after that element, such as `:last-child`,
`:only-child`, `:last-of-type`, `:only-of-type`, `:nth-last-child`, and
`:nth-last-of-type`.

When inserting or removing a node, previous siblings only need style
invalidation if one of them was matched against such a selector. Use the
parent-level flag to skip the previous-sibling walk when no child under
that parent can be affected.

This saves a lot of invalidation work on sites that insert a lot of
nodes into the DOM via JS.
2026-04-26 16:14:43 +02:00
Andreas Kling
13c81b4636 Tests: Split CI-timing-out structural-* stress tests by mode
Several structural-* stress tests iterated over enough scopes,
mutation cases, and modes to time out on the slower CI runners.
Split each timing-out test along mode boundaries so no single file
runs more than a quarter of the original work, mirroring the
warm/cold/pseudo/detached split shape across the suite:

* structural-descendant-stress: split scope-wise into doc/shadow,
  slot, and part files (already there from the earlier commit),
  then mode-wise into attached/detached and warm/pseudo halves.
* structural-descendant-state-stress: split mode-wise into the
  same four-quadrant attached/detached x warm/pseudo layout.
* structural-descendant-direction-language-stress: ditto.
* structural-child-part-shadow-stress and structural-child-slot-
  stress: same four-quadrant split (these already had no -cold
  detached modes so the detached half is smaller).

Every resulting file is well under 700 lines of expected output,
keeping the per-test runtime comfortably below the CI timeout.
2026-04-26 10:40:58 +02:00
Andreas Kling
89c0b124e4 Tests: Report invalidation counters without PASS/FAIL judgment
The two no-op-invalidation-counters tests asserted an exact target
count and printed FAIL when the live count didn't match. The numbers
they record document how much invalidation work happens for a given
mutation, and we expect those numbers to drop as the invalidation
logic improves rather than to be a fixed target right now. Keep the
tests as a moving record of current counts instead of as pass/fail
gates, so churn shows up in expected file diffs without making the
tests look broken.
2026-04-26 10:40:58 +02:00
Andreas Kling
2ff002b0bb LibWeb: Invalidate dir=auto descendant styles on text mutation
When the text content under a dir=auto ancestor changes, the
ancestor's effective directionality can flip and inherits down to
every descendant. The previous commit only marked the ancestor
itself dirty, so descendants whose :dir()-dependent style depended
on the ancestor's direction were left rendered against the old
value.

Walk the ancestor's shadow-including inclusive subtree and mark
every element for style update so the new direction propagates
through the cascade.
2026-04-26 10:40:58 +02:00
Andreas Kling
89aadba4d2 Tests: Add structural style invalidation matrix
A large suite of structural style-invalidation tests built around a
single shared driver (structural-matrix.js) that runs each scenario
under a matrix of subject/parent/ancestor topologies: light DOM,
shadow tree, ::slotted, ::part, exportparts, and SVG <use> shadow.

Covers:
 * child mutations under parents observable via :empty,
   :placeholder-shown, :nth-child, :has(), and ::part.
 * descendant mutations affecting :has(), :dir/:lang inheritance,
   slot reassignment, and dir=auto text changes.
 * detach/reconnect of subtrees, including reconnecting under a
   different shadow scope.
 * topology changes (slot name, exportparts, SVG <use> rebuild)
   that change which scope's rules apply to a subject.

The driver records baseline counter snapshots and asserts both the
visible style result and that the right invalidation work happens
(or doesn't) for each topology variant, so that future changes to
the invalidation pipeline can't silently regress any of them.
2026-04-26 10:40:58 +02:00
Andreas Kling
428a12dc25 Tests: Add style-attribute no-op invalidation counters test
Sets the style attribute to the same string twice, and to a
different value once, then uses the styleInvalidations counter to
assert that an identical re-set is a no-op while a real change
invalidates exactly once.
2026-04-26 10:40:58 +02:00
Andreas Kling
efb669677a Tests: Add inserted-subtree style computation test
When a subtree is inserted into a connected parent, every newly-
connected element needs to pick up matching style without falling
back to Node::inserted() unconditionally dirtying the world. Build
a small subtree off-document, insert its root, and assert that a
deeply-nested leaf and a freshly-inserted direct child both have
their cascade applied.
2026-04-26 10:40:58 +02:00
Andreas Kling
d1ec38524a Tests: Add child-state pseudo-class invalidation matrix
Exercises invalidation for child-state pseudo-classes
(:focus-within, :hover, :has(), :empty, :placeholder-shown,
:dir(), :valid/:invalid, :checked, :defined) across DOM mutations:
appendChild, removeChild, moveBefore (same-parent and
cross-parent), DocumentFragment inserts, and shadow-tree variants.
For each combination it verifies the parent's color (or pseudo-
element content) before and after the mutation, locking down the
expected invalidation behavior so future cleanup of the
invalidation pipeline doesn't silently regress these cases.
2026-04-26 10:40:58 +02:00
Andreas Kling
dd702d8beb Tests: Add child-change no-op invalidation counters test
Tracks the styleInvalidations counter to make sure inserting and
removing children (and document fragments) under a parent that has
no rules keying off child-state pseudo-classes doesn't dirty any
element's style.
2026-04-26 10:40:58 +02:00
Johan Dahlin
0de26af387 Tests/LibWeb: Add tests for @font-face load behavior 2026-04-25 17:06:28 +02:00
Tim Ledbetter
5d69c6d2b7 LibWeb: Filter by font width before weight in font matching
Implement the width filtering step of the font matching algorithm.
Without it, system font providers that group all widths under one
family could return a condensed variant for font-width: normal,
producing visibly narrower text.
2026-04-24 20:19:38 +02:00
Andreas Kling
928a5247ff LibWeb: Narrow stylesheet add/remove invalidation
Avoid broad document invalidation when adding or removing ordinary
document-owned or shadow-owned stylesheets. Reuse the targeted
StyleSheetInvalidation path for style rules, including shadow-host
escapes, pseudo-element-only selectors, and trailing-universal cases.

Keep the broad path for sheet contents whose effects are not captured
by selector invalidation alone, including @property, @font-face,
@font-feature-values, @keyframes, imported sheets, and top-level @layer
blocks. Broad-path shadow-root sheets still reach host-side consumers
through their active-scope effects.
2026-04-23 16:45:22 +02:00
Andreas Kling
cfa75e6eb4 LibWeb: Invalidate stylesheet owners when disabled state changes
Toggling CSSStyleSheet::disabled previously cleared the cached media
match bits and reloaded fonts, but never informed the owning documents
or shadow roots that style resolution was now stale. Worse, the IDL
binding for the disabled attribute dispatches through a non-virtual
setter on StyleSheet, so any override on CSSStyleSheet was bypassed
entirely.

Make set_disabled() virtual so the CSSStyleSheet override actually runs,
snapshot the pre-mutation shadow-root stylesheet effects before flipping
the flag, and hand them to invalidate_owners() so a disable that strips
the last host-reaching rule still tears down host-side style correctly.
2026-04-23 16:45:22 +02:00
Andreas Kling
a0dc0c61f4 LibWeb: Scope broad shadow-root stylesheet invalidation
When invalidate_owners() runs on a stylesheet scoped to a shadow root,
we previously dirtied the host and its light-DOM side too broadly. That
forced restyles on nodes the shadow-scoped stylesheet cannot match.

Inspect the sheet's effective selectors and dependent features up front.
Only dirty assigned nodes, the host, the host root, or host-side
animation consumers when the sheet can actually reach them, while
keeping purely shadow-local mutations inside the shadow tree.
2026-04-23 16:45:22 +02:00
Andreas Kling
4e92765211 LibWeb: Narrow @keyframes insertRule() invalidation
Handle inline stylesheet @keyframes insertions without falling back to
broad owner invalidation. Recompute only elements whose computed
animation-name already references the inserted keyframes name.

Document-scoped insertions still walk the shadow-including tree so
existing shadow trees pick up inherited animations, and shadow-root
stylesheets fan out through the host root so :host combinators can
refresh host-side consumers as well. Also introduce the shared
ShadowRootStylesheetEffects analysis so later stylesheet mutation paths
can reuse the same per-scope escape classification.
2026-04-23 16:45:22 +02:00
Andreas Kling
b6559d3846 LibWeb: Narrow inline stylesheet insertRule() invalidation
Avoid forcing a full style update when a connected inline <style> sheet
inserts an ordinary style rule. Build a targeted invalidation set from
the inserted rule and walk only the affected roots instead.

Introduce the shared StyleSheetInvalidation helper so later stylesheet
mutation paths can reuse the same selector analysis and root application
logic. It handles trailing-universal selectors, pseudo-element-only
rightmost compounds, and shadow-host escapes through ::slotted(...) and
:host combinators.

Keep the broad invalidate_owners() path for constructed stylesheets and
other sheet kinds whose TreeScope interactions still require it.
2026-04-23 16:45:22 +02:00
Andreas Kling
e29281893a LibWeb: Preserve pending style-update flags across document adoption
Adopting a node into another document preserves the node's dirty style
flags, but the destination ancestor chain never sees them propagate. If
a style update is already pending in the new document, it can skip the
adopted subtree entirely.

Snapshot the subtree and child dirty bits before set_document() updates
m_document, then walk the new ancestor chain and re-mark
child_needs_style_update so the pending restyle still descends into the
adopted subtree.
2026-04-23 16:45:22 +02:00
Andreas Kling
a94f9aa4c7 LibWeb: Filter non-inheriting registered custom properties on inherit
When inheriting custom-property data from a parent element, we were
copying the parent's full CustomPropertyData regardless of whether
each property was registered with `inherits: false`. That caused
non-inheriting registered properties to leak from the parent,
contrary to the @property spec.

Wrap the parent-side lookup so we strip any custom property whose
registration says it should not inherit, and only build a fresh
CustomPropertyData when at least one property was actually filtered.

Key the filtered view's cache on both the destination document's
identity and its custom-property registration generation. The
generation counter is local to each document, so a subtree adopted
into another document (or queried via getComputedStyle from another
window) could otherwise pick up a cached view computed under an
unrelated registration set and silently skip non-inheriting filtering
in the new document.
2026-04-22 20:59:00 +02:00
Andreas Kling
11c75a2ffb LibWeb: Fix @keyframes resolution for slotted elements
A @keyframes rule scoped to a shadow root was not reliably reached
from an animated slotted light-DOM element: the keyframes lookup
walked the element's own root first, then fell back to the document,
but slotted elements can pick up animation-name from a ::slotted(...)
rule that lives in an ancestor shadow root rather than in the
element's own tree.

Track the shadow-root scope that supplied each winning cascaded
declaration, and use that scope to resolve the matching @keyframes
when processing animation definitions. A shared constructable
stylesheet can be adopted into several scopes at once, so the
declaration object alone is too weak as a key; the per-entry
shadow-root pointer disambiguates which adoption actually contributed.

Also refresh running CSS animations' keyframe sets when style is
recomputed. Previously only the first animation creation path set a
keyframe set, so an existing animation never picked up newly inserted
@keyframes rules.
2026-04-22 20:59:00 +02:00
Andreas Kling
e4800b2498 LibWeb: Parse @keyframes name as logical string, not token text
The @keyframes parser was storing the keyframes name via
Token::to_string(), which keeps a string token in its quoted,
serialized form. That meant @keyframes "foo" was stored as
"\"foo\"" while animation-name: "foo" resolved to "foo",
and the two never matched.

Store the unquoted string or identifier value so the @keyframes name
and the animation-name reference compare on the same string.
2026-04-22 20:59:00 +02:00
Callum Law
76250ba142 LibWeb: Validate literal numeric values at parse time
This brings a couple of advantages:
 - Previously we relied on the caller validating the parsed value was in
   bounds after the fact - this was usually fine but there are a couple
   of places that it was forgotten (see the tests added in this commit),
   requiring the bounds to be passed as arguments makes us consider the
   desired range more explicitly.
 - In a future commit we will use the passed bounds as the clamping
   bounds for computed values, removing the need for the existing
   `ValueParsingContext` based method we have at the moment.
 - Generating code is easier with this approach
2026-04-22 14:24:12 +01:00
Andreas Kling
7a5b1d9de1 LibWeb: Delay generic :has() sibling scans until sibling roots
Mark elements reached by stepping through sibling combinators inside
:has() and use that breadcrumb during generic invalidation walks.

Keep the existing conservative sibling scans for mutations outside
those marked subtrees so nested :is(), :not(), and nesting cases
continue to invalidate correctly.

Also keep :has() eager within compounds that contain ::part(). Those
selectors retarget the remaining simple selectors to the part host, so
deferring :has() there changes which element the pseudo-class runs
against and can make ::part(foo):has(.match) spuriously match.

Add a counter-based sibling-scan test and a regression test covering
the ::part()/ :has() selector orderings.
2026-04-20 13:20:41 +02:00
Callum Law
f4e7e193da LibWeb: Implement composition for FitContentStyleValue 2026-04-09 21:41:49 +01:00
Callum Law
af17641b0a LibWeb: Implement interpolation for FitContentStyleValue 2026-04-09 21:41:49 +01:00
Sam Atkins
68aa529a18 Tests: Add a test for combinators inside pseudo selectors
:host() and ::slotted() both take a `<compound-selector>` as their
argument, but we currently allow a sneaky combinator in there too, as
this demonstrates. That will be solved over subsequent commits, but
adding this now to track progress.
2026-04-08 15:53:02 +01:00
Tim Ledbetter
2c728abd9f Tests: Use wider test string in FontFace arraybuffer matching test
This test relies on the width of the test font and the fallback font
being different to determine whether they matched or not. The
`offsetWidth` rounding change introduced in 51c7afdf5f caused these
widths to appear the same, meaning the test failed. This change avoids
the issue by using a longer string with a deliberately wide glyph.
2026-04-05 09:19:37 +02:00
Tim Ledbetter
5b584fde1d LibWeb: Register JS-created FontFace objects for font matching
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.
2026-04-05 00:13:35 +02:00
Tim Ledbetter
af6bc07c4f LibWeb/CSS: Resolve var() in keyframe animation-timing-function
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.
2026-04-01 11:38:48 +01:00
Aliaksandr Kalenik
b36f2361f1 Tests: Fix flaky iframe srcdoc test to wait for actual content
The test checked iframe.contentDocument?.readyState !== "complete" to
decide whether to wait for the iframe's load event. However, the
initial about:blank document has readyState "complete", so this check
passes immediately even when the srcdoc navigation hasn't activated
yet. Under heavy load with sanitizers, the srcdoc document activation
is delayed long enough for the test to proceed with the about:blank
document, causing a TypeError when querySelector("#target") returns
null.

Fix by waiting for the actual srcdoc content to appear rather than
relying on readyState. Use a while loop with { once: true } load
event listeners to handle the case where multiple load events fire
(one for about:blank, one for srcdoc).
2026-03-31 09:47:59 +02:00
Callum Law
03d479c1da LibWeb: Validate ASF syntax at parse time 2026-03-30 19:57:36 +01:00
Callum Law
071b000d9f LibWeb: Only allow ASFs in descriptor values if explicitly supported
`@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`.
2026-03-30 19:57:36 +01:00
Callum Law
8b66e7f463 LibWeb: Consider semicolon in parse_descriptor_value a SyntaxError
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
2026-03-30 19:57:36 +01:00
Callum Law
3e58e15217 LibWeb: Support relative lengths within color-mix percentage calc()s 2026-03-30 14:05:10 +01:00
Callum Law
fe5d6471f0 LibWeb: Store GridTrackPlacement sub-values as StyleValues
Gets us one step closer to removing the `FooOrCalculated` classes
2026-03-30 14:05:10 +01:00
Dylan Hart
1354eb1ac2 LibWeb: Resolve var() in shorthands before pseudo-element filtering
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>
2026-03-30 12:46:20 +01:00
Sam Atkins
7f322da78c Tests: Update our in-house @supports test to be clearer and add env()
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.
2026-03-29 21:27:20 +01:00
Tim Ledbetter
657060ccc2 LibWeb: Implement FontFaceSet.check()
This returns true if the given text can be rendered with the fonts in
the set that are fully loaded.
2026-03-27 15:28:59 +00:00
Callum Law
b86377b9dc LibWeb: Clamp CSS <integer> value to i32 at parse time
This matches the behavior of other browsers. Previously we implemented
this at used-value time for z-index specifically.
2026-03-26 12:30:01 +01:00
Callum Law
0ab06e119e LibWeb: Use i32 max for clamping 'infinite' calculated integers
This required us to change our range values from `float`s to `double`s
since `float` can't exactly represent i32 max
2026-03-26 12:30:01 +01:00
Callum Law
0e8956ee30 LibWeb: Round up on half when rounding to nearest integer in CSS 2026-03-26 12:30:01 +01:00