Instead of calling invalidate_style(CSSFontLoaded) which marks the
entire subtree for style recomputation, use set_needs_style_update(true)
to mark only individual elements that reference the loaded font family.
This is correct because element_uses_font_family() checks the computed
(inherited) font-family value, so descendants inheriting the font will
match individually, while descendants that override font-family to a
different font are skipped entirely.
When multiple descendant nodes change in one style invalidation cycle,
invalidate_style_of_elements_affected_by_has() walks from each pending
node up to all ancestors. Since ancestor paths converge going up, the
same ancestor elements get processed repeatedly, causing redundant
invalidate_style_if_affected_by_has() calls.
Replace the broad whole-subtree fallback for :has() invalidation with
a more targeted approach. The old code unconditionally overwrote
fine-grained :has() invalidation plans with invalidate_whole_subtree
for every non-rightmost compound containing :has(). This prevented
optimization for direct cases like `.a:has(.b) .c`.
The new approach propagates pseudo_class:Has through :is()/:where()
argument processing when :has() appears in non-rightmost compounds of
the inner selector. For complex :is() arguments (multiple compounds),
it falls back to whole-subtree invalidation since the outer plan can't
correctly capture the nested combinator structure.
The :nth-child(), :nth-last-child(), :nth-of-type(), and
:nth-last-of-type() pseudo-classes previously returned false for
elements that have no parent node. This meant that calling
element.matches(":nth-child(1)") on a standalone element created
via document.createElement() would incorrectly return false.
An element without a parent has no siblings, so its index is 1. The
simpler child-indexed pseudo-classes (:first-child, :last-child, etc.)
already handled this case correctly by checking for sibling presence
without requiring a parent. This change brings the nth-* variants in
line with that behavior by guarding the sibling iteration loops on
parent existence rather than returning false early.
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.
These pseudo-classes were missing from collect_properties_used_in_has,
which meant changes to the `required` attribute did not trigger
:has()-based ancestor invalidation.
While all parsed argument grammars can be represented as
`Vector<Vector<ComponentValue>>`, we can save some redundant work by
storing them in their argument-grammar-parsed format.
Note that for all currently implemented ASFs this is actually
`Vector<Vector<ComponentValue>>` and thus this change will only be
relevant for ASFs we haven't implemented yet
Split the structural-change selector metadata into directional bits for
first/last-child and forward/backward positional selectors.
This gives sibling invalidation enough information to distinguish which
side of a mutation can affect an element, instead of treating all
structural selectors as bidirectional.
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.
Cache the display list commands produced by each PaintableBox's paint()
on a per-phase basis. On subsequent display list rebuilds, if a
paintable's cache is still valid, replay the recorded commands directly
— skipping paint() and all the property resolution it entails.
Besides saving time on property resolution, this also enables Skia to
reuse path tessellation results across frames — e.g. border paths are
preserved in the cache and don't need to be re-tessellated on every
repaint.
Rename Document::set_needs_display() to set_needs_repaint() and make it
private. External callers must now go through Node/Paintable which
route the request to the document internally.
Fix one existing misuse in AnimationEffect that was calling
document-level set_needs_display() instead of routing through the
target element's paintable.
This is preparation for per-paintable display list command caching:
repaint requests must go through specific paintables so their cached
command lists can be invalidated.
When an image has no intrinsic dimensions but has an intrinsic aspect
ratio, the CSS default sizing algorithm should resolve its size as a
contain constraint against the default object size. Previously, we
returned the default size directly, which caused such images to stretch
to fill the entire background positioning area. The SVG's default
`preserveAspectRatio` would then center the content within that
oversized viewport, making the image appear horizontally mispositioned.
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 spec says that the `system` descriptor defaults to `symbolic` but
previously we just ignored any `@counter-style` rules without a `system`
descriptor
Previously we just passed around a reference to the `CounterStyle`
stored on `Document::registered_counter_styles` but this won't be
possible for anonymous counter styles (i.e. those created by the
`<symbols()>` function)
Add unsafe_layout_node(), unsafe_paintable(), and unsafe_paintable_box()
accessors that skip layout-staleness verification. These are for use in
contexts where accessing layout/paintable data is legitimate despite
layout not being up to date: tree construction, style recalculation,
painting, animation interpolation, DOM mutation, and invalidation
propagation.
Also add wrapper APIs on Node to centralize common patterns:
- set_needs_display() wraps if (unsafe_paintable()) ...set_needs_display
- set_needs_paint_only_properties_update() wraps similar
- set_needs_layout_update() wraps if (unsafe_layout_node()) ...
And add Document::layout_is_up_to_date() which checks whether layout
tree update flags are all clear.
We have a common pattern of creating a `WeakPtr<T>` from a reference and
passing that into a lambda, to then take the strong ref when the lambda
is executed. Add `weak_callback(Weakable, lambda)` that returns a lambda
that only invokes the callback if a strong ref exists, and passes it as
the first argument.
Stop converting between CSS and device pixels as part of rendering - the
display list should be as simple as possible, so convert to DevicePixels
once when constructing the display list.
Replace the unsafe HashTable<GC::Weak<DOM::Node>> with
GC::WeakHashSet<DOM::Node>. The null check in the iteration loop is
no longer needed since WeakHashSet's iterator skips dead entries.
As FontFaces are added or removed from a FontFaceSet, and as they load
or fail, the FontFaceSet moves them between a few different lists, and
updates its loading/loaded status. In the spec, this is how the
FontFaceSet.[[ReadyPromise]] gets fulfilled: When the document has
finished loading and FontFaceSet.[[LoadingFonts]] is empty, it resolves
the promise.
To support this, FontFace now keeps a set of FontFaceSets that it is
contained in.
This lets us remove the non-spec resolve_ready_promise() call in
EventLoop which was sometimes triggering before any fonts had attempted
to load.
As noted, there's a spec issue with the ready promise: If nothing
modifies the document's fonts, then it would never resolve. My ad-hoc
fix is to also switch the FontFaceSet to the loaded state if it is
empty, which appears to solve the issues but is not ideal.
The current css-font-loading spec doesn't define a constructor for this,
and from our own code we can rely on initializing things in the C++
constructor.
The spec requires us to follow the steps in FontFace::load() whenever a
font is loaded. The simplest way to do so, is to make that the only way
we load fonts. :^)
To support this, FontFace::load() uses its connected CSSFontFaceRule's
ParsedFontFace if it's available.
The 1 regression in generic-family-keywords-003.html seems to be a false
positive: we don't support the system-ui font keyword.
Corresponds to:
f0635e6e10
Assigning null to a non-nullable DOMString didn't make sense.
Apparently our bindings generator didn't care though. It's already
nullable on the C++ side by being an Optional<String>.