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.
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.
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().
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.
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.
Several invalidation paths need to consider not only a node's own root
scope, but also shadow scopes that can observe the node through :host(),
::slotted(), or ::part() selectors. Each caller open-coded that
traversal, which made the dir/lang and dir=auto fixes carry the same
shadow-boundary logic in multiple places.
Add Node helpers for resolving a node's style scope and for visiting
every style scope that may observe that node. Use them from the
property, child-list, dir/lang, and dir=auto invalidation paths, and
share the same style-scope lookup with DOM::AbstractElement and
Layout::NodeWithStyle.
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.
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.
::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.
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.
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.
We already had Internals counters for :has() invalidation work and
the high-level invalidation passes. Add four more that record how
often Element::recompute_style and recompute_inherited_style run,
and how often each bails out as a no-op. The new tests on this
branch use these counters to assert that mutations don't trigger
redundant style recomputation.
When we are recomputing inherited styles, we should already have
computed style for that element at least once, so cascaded and computed
properties should exist at that point. So make the invariant explicit.
`recompute_inherited_style()` assumed that there is no work to do if the
element has no layout node. This however is not necessarily true.
In particular, the following can happen:
1. The element in question is a descendant at least two layers below an
element that has `display: none`, i.e. with at least one other
element between them. (If it is a direct child, a full style
recomputation will be forced for unrelated reasons).
2. TreeBuilder decides to skip creating layout nodes for the `display:
none` element and all its descendants.
3. Due to some change on the ancestor (e.g. class added, id changed),
the value of a property that can be inherited changes on the
ancestor.
4. The property value of the descendant now also needs to change.
In that scenario, we won't compute the entire style of the descendant,
since that already happened. But we do need to update its inherited
properties because the old ones are now stale. At that point we still
don't have a layout node for the element since it will only be created
after this style update.
Instead of skipping the update for inherited properties, simply allow
`recompute_inherited_style()` to run even in absence of a layout node.
And then apply the style to the layout node only if one exists. If none
exists, the style will be applied later if and when a corresponding
layout node is created.
This partially fixes the blank main navigation menus on
https://bleepingcomputer.com.
Previously, the LibWeb bindings generator would output multiple per
interface files like Prototype/Constructor/Namespace/GlobalMixin
depending on the contents of that IDL file.
This complicates the build system as it means that it does not know
what files will be generated without knowledge of the contents of that
IDL file.
Instead, for each IDL file only generate a single Bindings/<IDLFile>.h
and Bindings/<IDLFile>.cpp.
Element::matches() and Element::closest() were re-parsing the selector
string on every call. The document already maintains a parsed-selector
cache for querySelector/querySelectorAll.
This patch folds that cache's lookup, parse, namespace filtering and
insertion behind a Document::parse_or_cache_selector_list(string)
and calls it from all four entry points. We also bump the cache's
limit to get more hits.
Saves 100ms of main thread time when loading the "insights" view on
our GitHub repo on my Linux machine. :^)
IntersectionObserver updates already iterate over each observer and its
observation targets. We then looked the same target and observer pair up
again through Element's registered observer list just to read and write
previousThresholdIndex and previousIsIntersecting.
Store that mutable state with the observer-side observation target
instead. The element-side list now only keeps strong observer
references for lifetime management and unobserve/disconnect.
This deviates from the spec's storage model, so document the difference
next to the preserved spec comments.
Previously this walked up the parent chain on every call, which shows
up as a 2.5% item in the profile while watching YouTube videos.
Cache an m_is_connected bit on Node instead, maintained by the DOM
insertion and removal steps.
The counter style used for an element (in either the `content` or
`list-style-type`) may change despite the computed values of properties
on that element remaining the same (e.g. if a new rule is inserted with
higher cascade precedence).
IntersectionObserver can keep elements from a navigated iframe's old
document alive until a later rendering update. Once that document tears
down its layout tree, descendant nodes and pseudo-elements can still
retain stale layout and paintable pointers, and destruction can bypass
the usual inactive-document teardown entirely.
Clear per-node layout and paintable pointers across the inactive
document subtree before tearing down the layout tree, and do the same
from destroy() for documents that never go through
did_stop_being_active_document_in_navigable().
Add a crash test that observes an iframe target, navigates the iframe,
and waits for rendering updates without touching stale layout state.
Fixes#8670
...instead of separate Element and PseudoElement arguments.
As noted, AbstractElement's constness is weird currently, but that's a
tangent I don't want to go on right now.
We maintain a registry of elements with an anchor-name so once they are
referenced for anchor positioning, we can find them with an O(1) lookup
instead of traversing the entire DOM tree.
Both Chromium and Gecko delay the document's load event for CSS image
resource requests (background-image, mask-image, etc). We now start
fetching CSS image resources as soon as their stylesheet is associated
with a document, rather than deferring until layout. This is done by
collecting ImageStyleValues during stylesheet parsing and initiating
their fetches when the stylesheet is added to the document.
Fixes#3448
When setting the cascaded, computed or custom properties on a pseudo
element, only ensure the pseudo element exists if there's actual data to
set. Similarly, we only remove the data if the pseudo element already
exists - no need to create one just to clear it of its data immediately
after.
Instead of immediately firing fullscreenchange, defer that until
WebContent's client has confirmed that it is in fullscreen for the
content. The fullscreenchange is fired by the viewport change, so in
cases where the fullscreen transition is instantaneous (i.e. the
fullscreen state is entered at the exact moment the viewport expands),
the resize event should precede the fullscreenchange event, as the spec
requires.
This fixes the WPT element-request-fullscreen-timing.html test, which
was previously succeeding by accident because we were immediately
fullscreenchange upon requestFullscreen() being called, instead of
following spec and doing the viewport (window) resize in parallel. The
WPT test was actually initially intended to assert that the
fullscreenchange event follows the resize event, but the WPT runner
didn't actually have a different resolution for normal vs fullscreen
viewports, so the resize event doesn't actually fire in their setup. In
our headless mode, the default viewport is 800x600, and the fullscreen
viewport is 1920x1080, so we do fire a resize event when entering
fullscreen. Therefore, that imported test is reverted to assert that
the resize precedes the fullscreenchange.
We were conflating elements being the active element and elements being
activated. The :active pseudo class is supposed to be based on whether
an element will have its activation behavior run upon a button being
released.
Store whether an element is being activated as a flag that is set/reset
by EventHandler.
Doing this allows label elements to visually activate their control
without doing a weird paintable hack, so the Labelable classes have
been yeeted.
Extract the repeated pattern of transforming a rectangle from absolute
coordinates to viewport coordinates via the accumulated visual context
into a helper method.
Change local-name computation in DOM::validate_and_extract() to preserve
everything after the first colon in a qualified name — matching a recent
spec change made in https://github.com/whatwg/dom/pull/1455 (and
replacing a previous spec requirement to use the “strictly split”
algorithm, which resulted in throwing away any other part of the name
after any second colon the name might have).
For the qualified name “f:o:o”, this change now gives localName=“o:o”.
Otherwise, without this change, it’d instead give localName=“o”.
This can save us some unnecessary recomputations if an element's style
changes from depending on a tree counting function to not.
Also removes an incorrect FIXME
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.
Introduce Document::update_layout_if_needed_for_node() which only calls
update_layout() when the node is connected. Use it at all call sites
that query layout metrics (offsets, client dimensions, image size, SVG
bounding box, etc.) so disconnected elements no longer trigger an
unnecessary layout.
With per-paintable display list command caching now in place, the
separate paint-only properties resolution phase is no longer needed.
Resolution now happens inline during painting and its cost is amortized
since it only runs on cache miss.
Move all property resolution to point of consumption:
- is_visible() and visible_for_hit_testing() compute on the fly
- Filter resolution moved to assign_accumulated_visual_contexts()
- Border radii, outlines computed on access
- Box shadows, backdrop filter resolved inline during painting
- Background resolution moved into paint_background()
- Mask resolution moved to StackingContext::paint()
- Text fragment and SVG stroke properties resolved during painting
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.
* Don't publicly expose methods that are only used internally.
* Modernize comment style (wrap at 120 chars, use NB for our notes).
* Change the stringifier for RequestFullscreenError to return a UTF-16
string. We were allocating a String, then transcoding it to UTF-16.
* Give helper methods clearer names, e.g.:
fullscreen_has_error_check -> is_element_allowed_to_enter_fullscreen
This was arguably put in a worse place by #8162; we mostly need the
device pixel offsets from the scroll state so keep track of those and
convert back to CSS pixels when necessary (i.e. scrollbar data).
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.