Commit Graph

2420 Commits

Author SHA1 Message Date
Callum Law
8e433ed742 LibWeb: Implement custom display interpolation
The newly failing tests in
`css-display/animations/display-interpolation.html` are due to the test
not having been updated for the new spec level and is in line with other
browsers.
2026-05-01 18:46:14 +02:00
Callum Law
2203c0d6aa LibWeb: Always store display as DisplayStyleValue
Previously values set via Typed-OM would be stored as
`KeywordStyleValue` which fell back to `inline` within
`ComputedProperties::display()`.
2026-05-01 18:46:14 +02:00
Callum Law
c84c944f20 LibWeb: Implement reification of DisplayStyleValue
Values which can be represented by a single keyword are reified as
`CSSKeywordValue` and those which can't are reified as `CSSStyleValue`
as before
2026-05-01 18:46:14 +02:00
Callum Law
1e8cc6bead LibWeb: Treat display: math as display: inline math
And serialize `inline math` as `math`. This matches the behavior of
other browsers.
2026-05-01 18:46:14 +02:00
Tim Ledbetter
d1fc4b4234 LibWeb: Route presentational hints through the CSS cascade
Previously, presentational hints bypassed the regular cascade pipeline
and wrote directly into `CascadedProperties` under
`CascadeOrigin::Author`. That meant `var()` substitution and the
invalid-at-computed-value-time fallback had to be duplicated in a
separate per-element pass, which in practice missed the IACVT step and
could leave a `GuaranteedInvalidStyleValue` in the cascaded
properties. This caused a crash in downstream code that assumed the
value had been resolved.

This introduces an `AuthorPresentationalHint` cascade origin and feeds
them through the cascade as normal declarations. This means that
`var()` resolution now happens in only one place.
2026-04-30 19:50:28 +01:00
Tim Ledbetter
297b1af9bf LibWeb: Extract per-declaration cascade loop into a reusable helper
This is preparatory work for routing presentational hints through the
same cascade pipeline as ordinary CSS declarations. Currently those
hints bypass `cascade_declarations()` and write into
`CascadedProperties` directly, which means `var()` substitution has to
be duplicated in a separate pass. To unify them at a single point, we
need to be able to feed an arbitrary property list.
2026-04-30 19:50:28 +01:00
Andreas Kling
0b4cea5b29 LibWeb: Coalesce safe :has() child-list invalidations
Avoid repeated :has() child-list scheduling when pending data already
covers every concrete feature bucket used by :has() selectors in a style
scope. Featureless-sensitive scopes record child-list mutations
conservatively instead.

That conservative path avoids scanning mutation subtrees for concrete
features when invalidation cannot rely on them anyway. When loading
the Intel ISA PDF in pdf.js, instrumented subtree feature-collection
visits at about 40k style invalidations dropped from around 71k to 1.6k,
saving ~650ms of main thread time on my Linux machine. :^)
2026-04-30 14:31:28 +02:00
Zaggy1024
ccaaefbf27 LibWeb+Meta: Don't require the custom ident blacklist parameter
With a default parameter, we can avoid having to store None for the
blacklist when it's not applicable.
2026-04-30 09:32:22 +02:00
Andreas Kling
59ae7d934a LibWeb: Skip unchanged :has() mutation fanout
Collect concrete features from pending :has() mutations and use them to
avoid re-invalidating non-subject :has() anchors whose matching rules
cannot be affected by the mutation.

Keep conservative behavior for shadow-boundary fanout, structural
sibling changes, and selectors whose relevant features cannot be proven.
For sibling-combinator relative selectors, avoid marking an anchor as
handled until it is actually invalidated, and make the sibling scan
respect anchors skipped by the feature filter for the same mutation.
2026-04-30 00:24:04 +02:00
Andreas Kling
d5dd64cf97 LibWeb: Move :has() mutation invalidation out of StyleScope
Keep StyleScope responsible for storing pending :has() mutation state,
but move the invalidation walk and scheduling helpers into
CSS::Invalidation::HasMutationInvalidator. This keeps the style scope
from owning the :has() invalidation algorithm directly and gives later
changes a narrower place to optimize.
2026-04-30 00:24:04 +02:00
Andreas Kling
c919f1c28f LibWebView: Add style invalidation counter dump option
Add --dump-style-invalidation-counters=N to Ladybird and propagate it
to WebContent helper processes.

When enabled, WebContent dumps the current document style invalidation
counters with dbgln() after every N recorded style invalidations. This
makes it possible to collect the counters while browsing without adding
temporary C++ logging.
2026-04-30 00:24:04 +02:00
Andreas Kling
09aefc2cd5 LibWeb: Move embedded content style invalidation into a helper
IFrame geometry changes and object representation changes directly
selected style invalidation reasons from their HTML element classes.
Move those mappings into a new
CSS::Invalidation::EmbeddedContentInvalidator.

The HTML elements continue to own their loading, representation, and
layout-tree side effects. CSS invalidation now owns the style dirtiness
associated with those embedded-content changes.
2026-04-29 15:47:23 +02:00
Andreas Kling
007dc28d16 LibWeb: Move shadow root style invalidation into the helper
Element::set_shadow_root directly selected the style invalidation reason
used when a shadow root changes. Move that mapping into
CSS::Invalidation::ElementStateInvalidator.

Element still owns the shadow-root state transition and the required
layout-tree invalidation. CSS invalidation now owns the style dirtiness
for that state change.
2026-04-29 15:47:23 +02:00
Andreas Kling
b0bb2bd0a8 LibWeb: Move active state invalidation into the helper
Element::set_being_activated directly selected the style invalidation
reason used for :active changes. Move that mapping into
CSS::Invalidation::ElementStateInvalidator.

The element continues to own its activation state. CSS invalidation now
owns the style invalidation work associated with changing that state.
2026-04-29 15:47:23 +02:00
Andreas Kling
ca08d5a901 LibWeb: Move custom state set invalidation into the helper
CustomStateSet directly selected the style invalidation reason used when
its JS-visible set is modified. Move that mapping into
CSS::Invalidation::CustomElementInvalidator.

This keeps the custom-state container focused on its set contents while
CSS invalidation owns the style work required by :state() selectors.
2026-04-29 15:47:23 +02:00
Andreas Kling
83dfed14ad LibWeb: Move input open style invalidation into the helper
HTMLInputElement still mapped its picker open-state change directly to a
style invalidation reason. Move that mapping into
CSS::Invalidation::ElementStateInvalidator alongside the matching select
open-state helper.

This keeps another element-state invalidation decision out of the HTML
implementation without changing the invalidation behavior.
2026-04-29 15:47:23 +02:00
Andreas Kling
c93dad7600 LibWeb: Move element state style invalidation into a helper
Several HTML element state changes directly selected style invalidation
reasons from their element implementations. Move those mappings into a
new CSS::Invalidation::ElementStateInvalidator helper.

This keeps details, dialog, option, and select code focused on their own
state changes while CSS invalidation owns the style work those changes
require. The existing invalidation breadth is preserved.
2026-04-29 15:47:23 +02:00
Andreas Kling
79c32f88d2 LibWeb: Move pending :has() invalidation into the helper
Document.cpp still flushed pending :has() invalidation by walking the
document and shadow-root style scopes directly. Move that CSS-specific
flush into CSS::Invalidation::HasMutationInvalidator.

Document continues to own the flag that says a :has() flush is needed.
The helper now owns the style-scope work needed to invalidate elements
affected by pending :has() mutations.
2026-04-29 15:47:23 +02:00
Andreas Kling
b8c2469566 LibWeb: Move media query style invalidation into a helper
Document.cpp still handled CSS fallout from stylesheet media query match
changes directly. Move active stylesheet evaluation, rule-cache
invalidation, shadow-root fallout, and slot propagation into the CSS
invalidation helper.

Document continues to decide when media queries should be evaluated.
The helper now owns the style invalidation consequences when stylesheet
media queries change match state.
2026-04-29 15:47:23 +02:00
Andreas Kling
0a938fdd51 LibWeb: Move slot style propagation into the helper
Document.cpp still knew how style changes on a slot propagate to
assigned light-DOM nodes. Move that flat-tree inheritance invalidation
into CSS::Invalidation::SlotInvalidator.

The style update walk continues to decide when an element's style
changed. The helper now owns the ::slotted() consequence of dirtying
assigned slottables for a changed slot.
2026-04-29 15:47:23 +02:00
Andreas Kling
98b13da3b4 LibWeb: Move adopted stylesheet invalidation into a helper
AdoptedStyleSheets.cpp still handled CSS-side fallout from adopted sheet
list mutations directly. Move sheet attachment and media-query setup to
a CSS invalidation helper, along with rule-cache invalidation.

The observable array callbacks continue to validate JS values and
same-document construction rules. The helper now owns the CSS work for
when a constructed stylesheet becomes visible to, or is removed from, a
Document or ShadowRoot.
2026-04-29 15:47:23 +02:00
Andreas Kling
c191d51af1 LibWeb: Move slotted style invalidation into a helper
Slottable.cpp still handled the style invalidation fallout from slot
assignment changes directly. Move that ::slotted()-related policy into
CSS::Invalidation::SlotInvalidator.

Slot assignment remains DOM bookkeeping. The helper now owns the
choice to dirty element slottables when they gain or lose assignment
to a slot.
2026-04-29 15:47:23 +02:00
Andreas Kling
bc0059cfd5 LibWeb: Move text directionality invalidation into the helper
CharacterData.cpp still handled the style invalidation fallout from text
mutations under dir=auto ancestors directly. Move that behavior into the
CSS language invalidation helper.

CharacterData continues to own text replacement and layout text updates.
The helper now owns the inherited :dir() restyle and :has(:dir(...))
ancestor scheduling that can follow from text content changes.
2026-04-29 15:47:23 +02:00
Andreas Kling
1ba3ec6ae7 LibWeb: Move part style invalidation into a helper
Element.cpp still handled the style invalidation fallout from part and
exportparts attribute changes directly. Move that ::part-related policy
into CSS::Invalidation::PartInvalidator.

Element continues to update the DOM token state for part attributes. The
helper now owns the style dirtiness for elements targeted through ::part
and for shadow-tree descendants exposed through exportparts.
2026-04-29 15:47:23 +02:00
Andreas Kling
b0effb3167 LibWeb: Move language style invalidation into a helper
Element.cpp still handled the CSS invalidation fallout from dir and lang
attribute changes directly. Move the descendant style dirtiness and
:has(:dir/:lang) ancestor scheduling into CSS::Invalidation.

Element continues to parse and store the DOM-facing attribute state. The
new helper owns the inherited style invalidation behavior that follows
from language and directionality changes.
2026-04-29 15:47:23 +02:00
Andreas Kling
3d8fa1ed26 LibWeb: Move node style invalidation into a helper
Node.cpp still contained the CSS policy for full style invalidation and
property-based invalidation plans. Move that logic into
CSS::Invalidation::NodeInvalidator.

Node remains the public DOM entry point for callers that need to
invalidate style. The helper now owns the :has() metadata probing,
style-scope plan lookup, and subtree/sibling invalidation scheduling.
2026-04-29 15:47:23 +02:00
Andreas Kling
61a18d91d6 LibWeb: Move pseudo-class state invalidation into a helper
Document.cpp contained the CSS rule-cache matching used to decide which
elements need style updates when hover, focus, or target state changes.
Move that logic into CSS::Invalidation::PseudoClassInvalidator.

Document still owns the current state slots and chooses when a state
transition happens. The helper now owns the selector matching and
recursive invalidation pass for those pseudo-class transitions.
2026-04-29 15:47:23 +02:00
Andreas Kling
6069bcdcc7 LibWeb: Move StyleInvalidator into CSS invalidation
StyleInvalidator applies CSS invalidation plans and matches selector
features while walking DOM nodes. Move the class from DOM into the
CSS::Invalidation namespace alongside the other invalidation helpers.

Document still owns the invalidator and DOM nodes still expose the state
that gets marked, but the policy for applying invalidation plans now has
a home with the rest of the CSS invalidation code.
2026-04-29 15:47:23 +02:00
Andreas Kling
4b3abc6958 LibWeb: Move invalidation set matching into a helper
Element.cpp still contained the CSS logic for deciding whether an
invalidation set references features present on an element. Move that
matcher into CSS::Invalidation::InvalidationSetMatcher.

The helper uses Element's public API for classes, id, attributes,
pseudo-class state, and removed-attribute tracking. This keeps Element
focused on DOM state while CSS::Invalidation owns selector feature
matching.
2026-04-29 15:47:23 +02:00
Andreas Kling
d7f5939e46 LibWeb: Move custom element state invalidation into a helper
Element.cpp still spelled out the :defined pseudo-class invalidation set
when custom element state changed. Move that selector policy into
CustomElementInvalidator.

This keeps Element responsible for the state transition, while
CSS::Invalidation owns the affected selector feature.
2026-04-29 15:47:23 +02:00
Andreas Kling
7c401b051b LibWeb: Move checked-state invalidation into a helper
HTMLInputElement had two call sites spelling out the same checked and
unchecked pseudo-class invalidation set. Move that selector policy into
FormControlInvalidator.

This keeps the input element responsible for detecting state changes,
while CSS::Invalidation owns the affected selector features.
2026-04-29 15:47:23 +02:00
Andreas Kling
84f4140226 LibWeb: Move hyperlink style invalidation into a helper
HTML and SVG link elements both encoded the same pseudo-class list for
hyperlink state changes. Move that CSS policy into LinkInvalidator and
have both call sites delegate to it.

This keeps element-specific code focused on detecting hyperlink state
changes, while the helper owns the affected selector features.
2026-04-29 15:47:23 +02:00
Andreas Kling
eeab3671c2 LibWeb: Move attribute style invalidation into a helper
Element.cpp still encoded the CSS consequences of attribute changes:
class/id invalidation keys, pseudo-class triggers, and shadow-host
stylesheet fallout. Move that policy into AttributeInvalidator.

Element now reports attribute changes to the helper and exposes a small
state hook to remember removed attributes while invalidation is pending.
2026-04-29 15:47:23 +02:00
Andreas Kling
85e33738f5 LibWeb: Move :has() element invalidation into the helper
Element exposed a small method that encoded how :has()-affected elements
are marked dirty. Move that policy into CSS::Invalidation alongside the
rest of the :has() mutation invalidation helpers.

This keeps Element focused on DOM state while preserving the existing
subject and non-subject :has() invalidation behavior.
2026-04-29 15:47:23 +02:00
Andreas Kling
95eb41092c LibWeb: Move structural mutation invalidation into a helper
Node.cpp still contained selector-specific policy for sibling and
same-parent-move structural invalidation. Move that logic into
CSS::Invalidation::StructuralMutationInvalidator so DOM mutation code
can delegate structural selector dependency handling.

This is a behavior-preserving extraction. It keeps the existing
previous-sibling walk guard, sibling-distance checks, shadow-root
marking, and ancestor child-needs-style propagation.
2026-04-29 15:47:23 +02:00
Andreas Kling
e4e3c46837 LibWeb: Move :has() mutation scheduling into a helper
Node.cpp still contained the policy for deciding when a DOM mutation
should schedule pending :has() invalidation work. Move that into
CSS::Invalidation::HasMutationInvalidator, next to the mutation feature
collector it depends on.

This keeps DOM mutation code focused on reporting that a mutation
happened, while CSS invalidation code owns the selector-specific checks
for :has() metadata and sibling-combinator sensitivity.
2026-04-29 15:47:23 +02:00
Andreas Kling
ea64c5e147 LibWeb: Move :has() mutation checks into a helper
Node.cpp currently knows too much about selector invalidation metadata
when deciding whether subtree mutations can affect :has() selectors.
Pull that logic into CSS/Invalidation/HasMutationFeatureCollector so DOM
mutation code can ask a focused helper instead of inspecting
StyleInvalidationData directly.

This is a behavior-preserving extraction. It keeps the existing
conservative fallbacks for featureless subtree-sensitive selectors and
still uses the existing element property matching helper for
pseudo-class metadata.
2026-04-29 15:47:23 +02:00
Callum Law
66c10ed190 LibWeb: Generate `<font-weight-absolute> parsing 2026-04-29 11:42:57 +01: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
Callum Law
44ad7e30b4 LibWeb: Generate <symbol> parsing 2026-04-29 11:42:57 +01:00
Callum Law
8849435d6f Meta+LibWeb: Initial scaffolding for CSS value type parsing code gen
In the future we should switch to using a better file format for this,
i.e. one that supports directly pasting CSS grammar production blocks
(https://drafts.csswg.org/css-values-4/#css-grammar-production-block)
and has support for inline comments, but we use JSON for now for
simplicity's sake.
2026-04-29 11:42:57 +01:00
Tim Ledbetter
324ed5de0d LibWeb: Use an OrderedHashMap for pending :has() invalidations
Every DOM mutation that may affect a :has() selector enqueues an
entry in StyleScope keyed by an ancestor node. The entries were
previously stored  in a Vector and linearly scanned on every insert to
deduplicate by node. We now use an OrderedHashMap instead, eliminating
the quadratic deduplication.
2026-04-29 09:34:24 +02:00
Andreas Kling
329a26307d LibWeb: Skip ::slotted matching for re-slotted slot elements
Per "find flattened slotables", a <slot> whose root is a shadow root is
recursed through, not appended to the result. ::slotted() in an outer
shadow must therefore not match such an intermediate slot.

Fixes the gallery on Reddit comment pages: a re-slotted <slot> was
picking up `::slotted(:not([slot])) { display: grid }` from the inner
shadow, which made the <ul> size to its content rather than the flex
container, leaving the carousel's "next" button with a 0px translate.
2026-04-29 04:54:11 +02:00
Tim Ledbetter
e495db44d5 LibWeb: Notify only affected layout nodes when a CSS image loads
Previously, `Document::notify_css_background_image_loaded()` walked the
entire `PaintableBox` subtree and cleared each box's paintable cache
whenever any CSS image finished loading.

Replace this with per-image observers owned by the layout node. During
`apply_style`, each node registers as an `ImageStyleValue::Client` for
the images its style references. On load, only the affected layout
node's paintables are invalidated.
2026-04-29 04:33:35 +02:00
Tim Ledbetter
d9f93013c9 LibWeb: Make ImageStyleValue::Client work with const ImageStyleValues 2026-04-29 04:33:35 +02:00
Andreas Kling
57d9668bca LibWeb: Don't treat first @media rule evaluation as a flip
CSSRuleList::evaluate_media_queries previously compared
CSSMediaRule::condition_matches() (which reads MediaQuery::m_matches,
default false) against the freshly-computed result. A brand-new @media
rule whose condition matches would therefore look like a false->true
flip the very first time it was evaluated, the same shape as the
CSSStyleSheet outer-MediaList bug fixed in the previous commit.

In practice all known paths that introduce a new @media rule
(StyleSheetList::add_sheet, AdoptedStyleSheets on_set,
CSSStyleSheet::invalidate_owners, CSSImportRule::set_style_sheet) call
through CSSStyleSheet::evaluate_media_queries eagerly and absorb the
flip before the next Document::evaluate_media_rules pass, so this
change does not move counters in the existing tests. It does make the
inner-@media handling consistent with the outer one, and protects any
future path (e.g. CSSGroupingRule::insert_rule into a nested rule
list) where a new @media rule might be evaluated for the first time
during the regular media-rule pass.

Track per-rule whether evaluate has been called yet via a sticky
m_did_evaluate flag on CSSMediaRule, and only record a flip on
subsequent evaluations.
2026-04-28 19:06:29 +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
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
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