Commit Graph

546 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
ba23d6a8ca LibWeb: Fix abspos containing block for split inline ancestors
When an inline-relative is split by block-level descendants, the rect
computation only looked at one anonymous wrapper and returned empty
for everything else, sending abspos placement back to the initial
containing block. On Reddit this let an inline-relative ad host's <a>
overlay the entire viewport and steal clicks from the post gallery's
pager buttons.

Walk every descendant of the inline's real block container instead,
collecting fragments from any InlineNode part of the inline plus the
border-box rects of its in-flow Box descendants. Matches what other
engines expose via getClientRects() for split inlines.

While here, narrow the inline-CB detection to the triggers that
actually apply on non-atomic inlines: `position`, `filter`,
`backdrop-filter` (and their will-change hints). transform, contain
and the rest don't apply per their specs - the broader check would
have started routing the WPT contain-paint/contain-layout ib-split
tests through the inline path once the rect computation began
returning non-empty results.
2026-04-29 04:54:11 +02: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
e9ed125bbe LibWeb: Don't fan sibling structural invalidation into descendants
When an insert or remove flips a sibling's match for :first-child,
:last-child, :nth-child, :nth-of-type, or a + / ~ combinator, only the
sibling's own match changes. Descendants only need rechecking when a
selector ties them to the sibling's position.

invalidate_structurally_affected_siblings used to mark the affected
sibling's entire subtree, so a sibling with N descendants turned a
match-state flip into N+1 element recomputes. Reuse the per-element
flags introduced for the same-parent-move optimization: when neither
affected_by_structural_pseudo_class_in_non_subject_position nor
affected_by_sibling_combinator_in_non_subject_position is set, mark
only the sibling root, and let the existing inherited-update path
reach descendants if the sibling's own style changes.

Rebaseline the impacted tests. styleInvalidations gains one per
affected sibling because set_needs_style_update increments the counter
(entire-subtree marks did not), but elementStyleRecomputations falls
accordingly.
2026-04-29 04:53:14 +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
f10f651e49 LibWeb: Don't treat first media-query evaluation as a flip
CSSStyleSheet::evaluate_media_queries previously flagged "no recorded
result yet" as a match-state change, so every freshly-loaded sheet
fired MediaQueryChangedMatchState on the first pass through
Document::evaluate_media_rules. For sheets added through
adoptedStyleSheets that piled an extra full-document style invalidation
on top of the AdoptedStyleSheetsList one, recomputing every element a
second time for nothing.

Drop the !has_value() leg so the very first evaluation establishes the
baseline silently. The sheet's rules already entered the cascade through
StyleSheetListAddSheet, AdoptedStyleSheetsList, or invalidate_owners,
each of which performs its own targeted invalidation.

Two callers relied on the implicit "first eval forces a refresh"
behavior to handle freshly-mutated state:

- invalidate_owners resets m_did_match, then leans on the next eval to
  repopulate it. With the new semantics it must also re-evaluate the
  sheet eagerly so MediaList::matches() and inner @media state are
  fresh before the next rule cache build reads them.
- The adoptedStyleSheets on_set callback didn't evaluate at all,
  relying on Document::evaluate_media_rules to populate
  MediaList::m_matches. That worked accidentally because the false
  flip retriggered invalidate_rule_cache after the matches had been
  populated. Mirror StyleSheetList::add_sheet by evaluating the sheet
  at adopt time so the rule cache build sees the correct match state
  even if it runs first (e.g. via a :has() invalidation pass).
2026-04-28 19:06:29 +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
ce5d0bdfc7 LibWeb: Narrow :has descendant invalidation fanout
When a :has() mutation is known to come from a specific subtree, use
that subtree as the mutation root while walking observed ancestors.

Before dirtying an anchor and its non-subject descendants, check whether
any cached :has() rule for that anchor can observe the changed subtree.
This keeps unrelated descendant mutations from invalidating every rule
that merely contains :has().
2026-04-28 15:34:49 +02:00
Andreas Kling
356a369aa6 LibWeb: Avoid descendant recomputes for same-parent moves
Moving a node within the same parent changes sibling and positional
relationships, but it does not make every descendant of the moved node
need a fresh computed style. Handle this as a structural mutation at
the old and new sibling edges and dirty only the moved root and the
affected ancestors, instead of marking the entire moved subtree.

Factor the existing previous- and next-sibling structural invalidation
out of Node::invalidate_style() into invalidate_structurally_affected_-
siblings(), and pull the ancestor child-needs-style-update walk into
mark_ancestors_as_having_child_needing_style_update(). The new
invalidate_style_after_same_parent_move() reuses both helpers.

Whether the moved root itself needs its own style recomputed depends
on whether any selector matched against it (or against a descendant)
relied on its position in the sibling list. Track that via two new
sticky bits on Element, set during selector matching:

  - m_affected_by_structural_pseudo_class_in_non_subject_position
  - m_affected_by_sibling_combinator_in_non_subject_position

Both are write-once (sticky) because matching descendants can set them
while we're not currently re-matching this element's own selectors;
keeping them set is conservative and avoids stale descendant style.

When neither bit is set and the element only carries subject-position
positional/sibling/has() involvement, we just dirty the root and skip
its descendants.

Rebaseline same-parent-move-root-only and the structural-feature filter
counters to reflect the new path. Matching behavior is unchanged.
2026-04-28 15:34:49 +02:00
Andreas Kling
11d9d4a7a5 LibWeb: Match attribute invalidation by presence and recent removal
Element::includes_properties_from_invalidation_set() previously
short-circuited and returned true for the id and class attributes,
because the parsed id and class-name state was treated as the source of
truth and the attribute presence check could disagree with it. That
shortcut over-invalidated for any [class] or [id] selector even when
neither attribute had ever been touched on the element.

Drop the special case and answer attribute presence queries the same
way for every attribute: the element either currently has the attribute,
or has had it removed earlier in this style update batch.

To handle the just-removed case, track the set of attribute names that
were removed since the last invalidation pass on a new per-Element
Vector<FlyString, 1> m_removed_attributes_for_style_invalidation. The
StyleInvalidator clears the vector for each element it visits during
perform_pending_style_invalidations, so it only ever holds names from
the current batch.

Rebaseline the affected attribute-presence tests; counters drop because
elements that never had id/class no longer match [class] / [id]
invalidations.
2026-04-28 15:34:49 +02:00
Andreas Kling
9fae2bcff9 LibWeb: Avoid unrelated structural :has invalidation
Track the simple selector features that appear inside :has() arguments
on each StyleScope, then consult that metadata before scheduling an
ancestor walk for a structural mutation. If the mutated subtree has no
tag, id, class, attribute, or pseudo-class feature that any cached
:has() argument cares about, skip the walk entirely.

Stay conservative for featureless-sensitive arguments such as :has(*),
:has(:not(...)), :has(:empty), and child-index pseudos: an unfeatured
node can still start or stop matching there. Track that case via a new
has_selectors_sensitive_to_featureless_subtree_changes flag on
StyleInvalidationData and fall back to the old conservative walk.

Stay conservative for pseudo-classes the subtree filter cannot probe
(:focus, :hover, validation pseudos). Move :default out of the set of
trackable feature pseudo-classes for the same reason; it now triggers
the conservative walk where it previously recorded metadata.

Tag and attribute names are stored lowercased, so for non-HTML elements
(SVG, MathML) treat lowercased matches as scheduling hints only; the
actual :has() match still goes through case-sensitive selector matching.

Test counter expectations are rebaselined to reflect the skipped walks
and reduced recomputations. Matching behavior is unchanged.
2026-04-28 15:34:49 +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
5ff832081d LibWeb: Make :dir() and :lang() invalidation cross shadow boundaries
Three related fixes around how dir and lang attribute changes flow
through shadow boundaries:

* Element::lang() looked up the host through parent_element(), which
  is null for elements directly inside a shadow root. The shadow
  root -> host fallback in step 3 therefore never fired and
  shadow-tree elements never inherited their host document's
  language. Walk through parent() instead so the shadow root case is
  detected.

* The dir/lang attribute_changed handlers each marked descendants
  for style update, but neither one descended through shadow
  boundaries via for_each_shadow_including_inclusive_descendant the
  same way (and only one cleared the cached language). Merge both
  handlers so dir and lang share the same shadow-including descendant
  walk.

* :has(:dir(...)) and :has(:lang(...)) on ancestors aren't keyed on
  any property the regular invalidation plan tracks, so the :has()
  ancestor walk has to be scheduled explicitly. Schedule it on the
  document scope and on every shadow scope reachable from this
  element via parent_or_shadow_host.
2026-04-26 10:40:58 +02:00
Andreas Kling
43f2021484 LibWeb: Recompute shadow part style when exportparts changes
When a shadow host's exportparts attribute changes, elements with
part tokens inside its shadow tree may newly become or stop being
targets of ::part() rules in the outer scope. Mark every such
descendant for style update so the new exposure is reflected.
2026-04-26 10:40:58 +02:00
Andreas Kling
48c4a254a6 LibWeb: Recompute style when an element's part attribute changes
::part(...) rules in the outer scope target shadow descendants by
their part-name tokens. When an element's part attribute changed,
m_parts and the IDL part list were updated but the element itself
was never marked for style update, so previously-matched ::part()
rules kept applying or new matches stayed unevaluated.
2026-04-26 10:40:58 +02:00
Andreas Kling
996080eeea LibWeb: Recompute style for slottables when slot assignment changes
assign_slottables only updated each slottable's m_assigned_slot
pointer. The slottable's match against ::slotted(...) rules is
determined by whether it's currently assigned to a slot, so gaining
or losing an assignment must trigger a fresh style computation.
Without that, mutations like changing a slot's name or a slottable's
slot attribute left the slottable rendered against stale ::slotted()
rules.

Mark the slottable element for style update both when it loses an
assignment and when it gains one.
2026-04-26 10:40:58 +02:00
Andreas Kling
09c0caf986 LibWeb: Build invalidation sets from ::slotted() simple selectors
build_invalidation_sets_for_simple_selector ignored ::slotted()
pseudo-element selectors entirely. As a result, the invalidation
plans built for a shadow scope didn't list class, attribute, or
pseudo-class properties referenced inside ::slotted(), so changing
those properties on a slottable couldn't enqueue an invalidation
plan for the rule.

Recurse into ::slotted()'s compound argument and feed each simple
selector through the same invalidation-set builder.
2026-04-26 10:40:58 +02:00
Andreas Kling
7d6f77bdf3 LibWeb: Recompute style for descendants when dir/lang changes
The dir and lang attributes inherit, so descendants' :dir() and
:lang() matches and any direction-dependent layout/text depend on
ancestor values. Element::attribute_changed updated the m_dir field
and refreshed cached language values, but never marked descendants
for style recomputation, so existing styled descendants kept their
old :dir()/:lang() results.

Mark every descendant for style update when dir or lang changes.
2026-04-26 10:40:58 +02:00
Andreas Kling
3cbe6bc89a LibWeb: Schedule attribute :has() walks on outer scopes too
The property-based Node::invalidate_style only added inner shadow
scopes (the element's own shadow and enclosing hosts' shadows). When
the mutated element lived inside a shadow tree, mutations could not
reach ::part(...:has(...)) rules in the outer document or outer
shadow root because those scopes were never collected.

Walk parent_or_shadow_host and add the document scope (or each
crossed shadow root scope) when the ancestor's root differs from
this element's root, alongside the inner shadow scopes already
collected.
2026-04-26 10:40:58 +02:00
Andreas Kling
fead65d9b2 LibWeb: Recurse into ::slotted() args when collecting :has() metadata
collect_properties_used_in_has only inspected pseudo-class argument
selectors. With ::slotted(.x:has(.descendant)) rules, the property
references inside the :has() argument were therefore never recorded
as :has()-affecting, so attribute and state changes on a slottable's
descendants couldn't enqueue an invalidation plan that covered the
::slotted() rule.

Recurse into the ::slotted() compound argument so the :has() metadata
maps include the properties used inside.
2026-04-26 10:40:58 +02:00
Andreas Kling
85a9239faf LibWeb: Make :has() invalidation reach ::slotted/:host rules
Two related fixes that together let :host(...:has(...)) and
::slotted(.x:has(...)) rules re-evaluate when their light-DOM input
changes:

* StyleScope::collect_selector_insights only recursed into
  pseudo-class argument selectors. The :has() inside a
  ::slotted(...) compound argument was therefore invisible to a
  shadow scope's insights, so may_have_has_selectors() reported
  false and Node::invalidate_style skipped the :has() walk for that
  scope. Walk the ::slotted() argument selector through the same
  insight collection.

* Element::invalidate_style_if_affected_by_has only set
  needs_style_update for elements in subject position. Inside
  ::slotted(.x:has(...)) and :host(...:has(...)) the rule's
  selector subject is the slot or host, but the styled element is
  this element; when the :has() result flips, this element's own
  computed style must be recomputed. Mark it dirty in the
  non-subject branch as well.
2026-04-26 10:40:58 +02:00
Andreas Kling
60113a3be2 LibWeb: Schedule attribute :has() walks on enclosing shadow scopes
The property-based Node::invalidate_style only collected the element's
own shadow scope (when the element itself was a shadow host), not the
shadow scopes of enclosing hosts. So a class or attribute change on a
light-DOM descendant of a shadow host could not flip
:host(...:has(.descendant)) rules in that host's shadow root.

Walk parent_or_shadow_host from the mutated element and add every
enclosing shadow root's StyleScope to the set we check :has()
metadata against, schedule walks on, and run invalidation plans
through.
2026-04-26 10:40:58 +02:00
Andreas Kling
bc4ff9038a LibWeb: Schedule :has() walks on enclosing shadow scopes for inserts
Light-DOM mutations under a shadow host can flip
:host(...:has(...)) rules that live entirely in the host's shadow
root. Node::invalidate_style only scheduled the :has() walk on the
root scope of the mutated node, so those shadow-side rules never
re-evaluated when their light-DOM input changed.

Walk up parent_or_shadow_host from the mutation parent and schedule
the :has() walk on every enclosing shadow root's StyleScope that has
:has() rules.
2026-04-26 10:40:58 +02:00
Andreas Kling
b8573eba6f LibWeb: Use document scope for invalidation in user-agent shadow trees
SVG <use> shadow trees opt into the host document's stylesheets via
ShadowRoot::uses_document_style_sheets. Style computation already
honors this in AbstractElement::style_scope(), but
Node::invalidate_style picked the shadow root's own StyleScope based
solely on root().is_shadow_root(), so mutations inside such trees
never consulted document :has() metadata or invalidation plans.

Match the existing style-computation scope selection so invalidation
runs against the document StyleScope when the root is a shadow root
that uses document stylesheets.
2026-04-26 10:40:58 +02:00
Andreas Kling
5461b44d04 LibWeb: Invalidate shadow subtree on host attribute change
Rules in a shadow root that match :host(...) can apply different style
to shadow descendants when the host's attributes or classes change.
The host's own invalidation flow doesn't reach into the shadow tree,
so descendants kept their cascaded values from the previous host
state.

When the host's stylesheets contain :host()-style rules that may match
the shadow host, mark the entire shadow subtree dirty so descendant
style is recomputed against the new host state.
2026-04-26 10:40:58 +02:00
Andreas Kling
5268b5a5b7 LibWeb: Run :has() invalidation when character data is inserted
Node::invalidate_style was bailing out for character data nodes before
scheduling :has() ancestor invalidation. Inserting or removing a text
node changes the parent's :empty state and can flip ancestor
:has(:empty) selectors, but with the early return in place those
ancestors were never marked for re-evaluation.

Move the early return below the :has() walk so character data still
participates in ancestor :has() invalidation. The text node itself
still has no style of its own, so the rest of the per-node work is
skipped as before.
2026-04-26 10:40:58 +02:00
Andreas Kling
5904a21a56 LibWeb: Clear pseudo-element style data when no rule matches
When computing pseudo-element style and no pseudo-element rules match,
StyleComputer was returning early without clearing the cascaded and
custom property data on the AbstractElement. As a result,
getComputedStyle() on the pseudo-element kept exposing values from a
previous matching state.

Clear both before bailing so a transition from matched to unmatched
leaves the pseudo-element in a clean state.
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
b27c6d68e8 LibWeb: Dedup @font-face fetches by source URL 2026-04-25 17:06:28 +02:00
Johan Dahlin
d90c5b295a LibWeb: Honor text argument of FontFaceSet.load() / .check() 2026-04-25 17:06:28 +02:00
Johan Dahlin
8c49029bb7 LibGfx+LibWeb: Only trigger @font-face loads from text shaping 2026-04-25 17:06:28 +02:00
Johan Dahlin
acabf765c1 LibWeb+LibGfx: Defer @font-face fetches until a codepoint renders 2026-04-25 17:06:28 +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