blob: 3e6446b9998c4822ced6cae1259bb01061522343 [file] [log] [blame]
/*
* Copyright (C) 1999 Lars Knoll ([email protected])
* (C) 1999 Antti Koivisto ([email protected])
* (C) 2001 Peter Kelly ([email protected])
* (C) 2001 Dirk Mueller ([email protected])
* (C) 2007 David Smith ([email protected])
* Copyright (C) 2004-2025 Apple Inc. All rights reserved.
* (C) 2007 Eric Seidel ([email protected])
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Library General Public
* License as published by the Free Software Foundation; either
* version 2 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Library General Public License for more details.
*
* You should have received a copy of the GNU Library General Public License
* along with this library; see the file COPYING.LIB. If not, write to
* the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
* Boston, MA 02110-1301, USA.
*/
#include "config.h"
#include "StyleTreeResolver.h"
#include "AXObjectCache.h"
#include "AnchorPositionEvaluator.h"
#include "CSSFontSelector.h"
#include "CSSPositionTryRule.h"
#include "CSSSerializationContext.h"
#include "ComposedTreeAncestorIterator.h"
#include "ComposedTreeIterator.h"
#include "Document.h"
#include "DocumentInlines.h"
#include "DocumentTimeline.h"
#include "HTMLBodyElement.h"
#include "HTMLInputElement.h"
#include "HTMLMeterElement.h"
#include "HTMLNames.h"
#include "HTMLProgressElement.h"
#include "HTMLSlotElement.h"
#include "LoaderStrategy.h"
#include "LocalFrame.h"
#include "MatchResultCache.h"
#include "NodeInlines.h"
#include "NodeRenderStyle.h"
#include "Page.h"
#include "PlatformStrategies.h"
#include "PositionTryFallback.h"
#include "PositionTryOrder.h"
#include "PositionedLayoutConstraints.h"
#include "Quirks.h"
#include "RenderElement.h"
#include "RenderStyleSetters.h"
#include "RenderView.h"
#include "ResolvedStyle.h"
#include "Settings.h"
#include "ShadowRoot.h"
#include "StyleAdjuster.h"
#include "StyleBuilder.h"
#include "StyleFontSizeFunctions.h"
#include "StyleOriginatedTimelinesController.h"
#include "StyleResolver.h"
#include "StyleScope.h"
#include "Text.h"
#include "TypedElementDescendantIteratorInlines.h"
#include "ViewTransition.h"
#include "WebAnimationTypes.h"
#include "WebAnimationUtilities.h"
#include "dom/EventTarget.h"
namespace WebCore {
namespace Style {
DEFINE_ALLOCATOR_WITH_HEAP_IDENTIFIER(TreeResolverScope);
TreeResolver::TreeResolver(Document& document, std::unique_ptr<Update> update)
: m_document(document)
, m_update(WTFMove(update))
{
}
TreeResolver::~TreeResolver()
{
m_document->styleScope().updateAnchorPositioningStateAfterStyleResolution();
}
TreeResolver::Scope::Scope(Document& document, Update& update)
: resolver(document.styleScope().resolver())
{
document.setIsResolvingTreeStyle(true);
// Ensure all shadow tree resolvers exist so their construction doesn't depend on traversal.
for (auto& shadowRoot : document.inDocumentShadowRoots())
const_cast<ShadowRoot&>(shadowRoot).styleScope().resolver();
selectorMatchingState.containerQueryEvaluationState.styleUpdate = &update;
}
TreeResolver::Scope::Scope(ShadowRoot& shadowRoot, Scope& enclosingScope)
: resolver(shadowRoot.styleScope().resolver())
, shadowRoot(&shadowRoot)
, enclosingScope(&enclosingScope)
{
selectorMatchingState.containerQueryEvaluationState = enclosingScope.selectorMatchingState.containerQueryEvaluationState;
}
TreeResolver::Scope::~Scope()
{
if (!shadowRoot)
resolver->document().setIsResolvingTreeStyle(false);
}
TreeResolver::Parent::Parent(Document& document)
: element(nullptr)
, style(*document.initialContainingBlockStyle())
{
}
TreeResolver::Parent::Parent(Element& element, const RenderStyle& style, OptionSet<Change> changes, DescendantsToResolve descendantsToResolve, IsInDisplayNoneTree isInDisplayNoneTree)
: element(&element)
, style(style)
, changes(changes)
, descendantsToResolve(descendantsToResolve)
, isInDisplayNoneTree(isInDisplayNoneTree)
{
}
void TreeResolver::pushScope(ShadowRoot& shadowRoot)
{
m_scopeStack.append(adoptRef(*new Scope(shadowRoot, scope())));
}
void TreeResolver::pushEnclosingScope()
{
ASSERT(scope().enclosingScope);
m_scopeStack.append(*scope().enclosingScope);
}
void TreeResolver::popScope()
{
return m_scopeStack.removeLast();
}
ResolvedStyle TreeResolver::styleForStyleable(const Styleable& styleable, ResolutionType resolutionType, const ResolutionContext& resolutionContext, const RenderStyle* existingStyle)
{
if (resolutionType == ResolutionType::AnimationOnly && styleable.lastStyleChangeEventStyle() && !styleable.hasPropertiesOverridenAfterAnimation())
return { RenderStyle::clonePtr(*styleable.lastStyleChangeEventStyle()) };
auto& element = styleable.element;
if (auto optionStyle = tryChoosePositionOption(styleable, existingStyle))
return WTFMove(*optionStyle);
if (resolutionType == ResolutionType::FastPathInherit) {
// If the only reason we are computing the style is that some parent inherited properties changed, we can just copy them.
auto style = RenderStyle::clonePtr(*existingStyle);
style->fastPathInheritFrom(parent().style);
m_document->styleScope().matchResultCache().updateForFastPathInherit(element, parent().style);
return { WTFMove(style) };
}
auto unadjustedStyle = [&] {
if (element.hasCustomStyleResolveCallbacks()) {
RenderStyle* shadowHostStyle = scope().shadowRoot ? m_update->elementStyle(*scope().shadowRoot->host()) : nullptr;
if (auto customStyle = element.resolveCustomStyle(resolutionContext, shadowHostStyle))
return WTFMove(*customStyle);
}
if (resolutionType == ResolutionType::FullWithMatchResultCache) {
if (auto cachedResult = m_document->styleScope().matchResultCache().resultWithCurrentInlineStyle(element)) {
auto result = scope().resolver->unadjustedStyleForCachedMatchResult(element, resolutionContext, WTFMove(*cachedResult));
MatchResultCache::update(*cachedResult, *result.style);
return result;
}
}
auto result = scope().resolver->unadjustedStyleForElement(element, resolutionContext);
m_document->styleScope().matchResultCache().set(element, result);
return result;
}();
if (unadjustedStyle.relations)
commitRelations(WTFMove(unadjustedStyle.relations), *m_update);
auto style = WTFMove(unadjustedStyle.style);
// Fully custom styles in UA shadow trees that don't originate from selector matching don't need adjusting.
if (unadjustedStyle.matchResult) {
Adjuster adjuster(m_document, *resolutionContext.parentStyle, resolutionContext.parentBoxStyle, &element);
adjuster.adjust(*style);
}
return {
.style = WTFMove(style),
.relations = { },
.matchResult = WTFMove(unadjustedStyle.matchResult)
};
}
static void resetStyleForNonRenderedDescendants(Element& current)
{
auto descendants = descendantsOfType<Element>(current);
for (auto it = descendants.begin(); it != descendants.end();) {
if (it->needsStyleRecalc()) {
it->resetComputedStyle();
it->resetStyleRelations();
it->setHasValidStyle();
}
if (it->childNeedsStyleRecalc()) {
it->clearChildNeedsStyleRecalc();
it.traverseNext();
} else
it.traverseNextSkippingChildren();
}
current.clearChildNeedsStyleRecalc();
}
static bool affectsRenderedSubtree(Element& element, const RenderStyle& newStyle)
{
if (newStyle.display() != DisplayType::None)
return true;
if (element.renderOrDisplayContentsStyle())
return true;
if (element.rendererIsNeeded(newStyle))
return true;
return false;
}
auto TreeResolver::computeDescendantsToResolve(const ElementUpdate& update, const RenderStyle* existingStyle, Validity validity) const -> DescendantsToResolve
{
if (parent().descendantsToResolve == DescendantsToResolve::All)
return DescendantsToResolve::All;
if (validity >= Validity::SubtreeInvalid)
return DescendantsToResolve::All;
if (update.changes && existingStyle) {
auto customPropertyInStyleContainerQueryChanged = [&] {
auto& namesInQueries = scope().resolver->ruleSets().customPropertyNamesInStyleContainerQueries();
for (auto& name : namesInQueries) {
// Any descendant may depend on this changed custom property via a style query.
if (!existingStyle->customPropertyValueEqual(*update.style, name))
return true;
}
return false;
}();
if (customPropertyInStyleContainerQueryChanged)
return DescendantsToResolve::All;
}
if (update.changes.containsAny({ Change::Container, Change::Renderer }))
return DescendantsToResolve::All;
if (update.changes.containsAny(inheritedChanges()))
return DescendantsToResolve::Children;
if (update.changes.contains(Change::NonInherited))
return DescendantsToResolve::ChildrenWithExplicitInherit;
ASSERT(!update.changes);
return DescendantsToResolve::None;
};
static bool styleChangeAffectsRelativeUnits(const RenderStyle& style, const RenderStyle* existingStyle)
{
if (!existingStyle)
return true;
return !existingStyle->fontCascadeEqual(style)
|| existingStyle->computedLineHeight() != style.computedLineHeight();
}
auto TreeResolver::resolveElement(Element& element, const RenderStyle* existingStyle, ResolutionType resolutionType) -> std::pair<ElementUpdate, DescendantsToResolve>
{
if (m_didSeePendingStylesheet && !element.renderOrDisplayContentsStyle() && !m_document->isIgnoringPendingStylesheets()) {
m_document->setHasNodesWithMissingStyle();
return { };
}
if (resolutionType == ResolutionType::RebuildUsingExisting) {
return {
ElementUpdate { RenderStyle::clonePtr(*existingStyle), Change::Renderer },
DescendantsToResolve::RebuildAllUsingExisting
};
}
auto resolutionContext = makeResolutionContext();
Styleable styleable { element, { } };
auto resolvedStyle = styleForStyleable(styleable, resolutionType, resolutionContext, existingStyle);
generatePositionOptionsIfNeeded(resolvedStyle, styleable, resolutionContext);
updateForPositionVisibility(*resolvedStyle.style, styleable);
auto update = createAnimatedElementUpdate(WTFMove(resolvedStyle), styleable, parent().changes, resolutionContext, parent().isInDisplayNoneTree);
if (!affectsRenderedSubtree(element, *update.style)) {
styleable.setLastStyleChangeEventStyle(nullptr);
if (update.style->display() == DisplayType::None && element.hasDisplayNone())
return { WTFMove(update), DescendantsToResolve::None };
return { };
}
auto descendantsToResolve = computeDescendantsToResolve(update, existingStyle, element.styleValidity());
bool isDocumentElement = &element == m_document->documentElement();
if (isDocumentElement) {
if (styleChangeAffectsRelativeUnits(*update.style, existingStyle)) {
// "rem" units are relative to the document element's font size so we need to recompute everything.
scope().resolver->invalidateMatchedDeclarationsCache();
descendantsToResolve = DescendantsToResolve::All;
}
}
// This is needed for resolving color:-webkit-text for subsequent elements.
// FIXME: We shouldn't mutate document when resolving style.
if (&element == m_document->body())
m_document->setTextColor(update.style->visitedDependentColor(CSSPropertyColor));
// FIXME: These elements should not change renderer based on appearance property.
if (RefPtr input = dynamicDowncast<HTMLInputElement>(element); (input && input->isSearchField())
|| is<HTMLMeterElement>(element)
|| is<HTMLProgressElement>(element)) {
if (existingStyle && update.style->usedAppearance() != existingStyle->usedAppearance()) {
update.changes.add(Change::Renderer);
descendantsToResolve = DescendantsToResolve::All;
}
}
auto resolveAndAddPseudoElementStyle = [&](const PseudoElementIdentifier& pseudoElementIdentifier) {
auto pseudoElementUpdate = resolvePseudoElement(element, pseudoElementIdentifier, update, parent().isInDisplayNoneTree);
auto pseudoElementChanges = [&]() -> OptionSet<Change> {
if (pseudoElementUpdate) {
if (pseudoElementIdentifier.pseudoId == PseudoId::WebKitScrollbar)
return pseudoElementUpdate->changes;
if (!pseudoElementUpdate->changes)
return { };
return { Change::NonInherited };
}
if (!existingStyle || !existingStyle->getCachedPseudoStyle(pseudoElementIdentifier))
return { };
// If ::first-letter goes aways rebuild the renderers.
if (pseudoElementIdentifier.pseudoId == PseudoId::FirstLetter)
return { Change::Renderer };
return { Change::NonInherited };
}();
update.changes |= pseudoElementChanges;
if (!pseudoElementUpdate)
return pseudoElementChanges;
if (pseudoElementUpdate->recompositeLayer)
update.recompositeLayer = true;
update.style->addCachedPseudoStyle(WTFMove(pseudoElementUpdate->style));
return pseudoElementUpdate->changes;
};
if (resolveAndAddPseudoElementStyle({ PseudoId::FirstLine }))
descendantsToResolve = DescendantsToResolve::All;
if (resolveAndAddPseudoElementStyle({ PseudoId::FirstLetter }))
descendantsToResolve = DescendantsToResolve::All;
if (resolveAndAddPseudoElementStyle({ PseudoId::WebKitScrollbar }))
descendantsToResolve = DescendantsToResolve::All;
resolveAndAddPseudoElementStyle({ PseudoId::Marker });
resolveAndAddPseudoElementStyle({ PseudoId::Before });
resolveAndAddPseudoElementStyle({ PseudoId::After });
resolveAndAddPseudoElementStyle({ PseudoId::Backdrop });
if (isDocumentElement && m_document->hasViewTransitionPseudoElementTree()) {
resolveAndAddPseudoElementStyle({ PseudoId::ViewTransition });
RefPtr activeViewTransition = m_document->activeViewTransition();
ASSERT(activeViewTransition);
for (auto& name : activeViewTransition->namedElements().keys()) {
resolveAndAddPseudoElementStyle({ PseudoId::ViewTransitionGroup, name });
resolveAndAddPseudoElementStyle({ PseudoId::ViewTransitionImagePair, name });
resolveAndAddPseudoElementStyle({ PseudoId::ViewTransitionNew, name });
resolveAndAddPseudoElementStyle({ PseudoId::ViewTransitionOld, name });
}
}
#if ENABLE(TOUCH_ACTION_REGIONS)
// FIXME: Track this exactly.
if (update.style->touchActions() != TouchAction::Auto && !m_document->quirks().shouldDisablePointerEventsQuirk())
m_document->setMayHaveElementsWithNonAutoTouchAction();
#endif
#if ENABLE(EDITABLE_REGION)
if (update.style->usedUserModify() != UserModify::ReadOnly)
m_document->setMayHaveEditableElements();
#endif
return { WTFMove(update), descendantsToResolve };
}
inline bool supportsFirstLineAndLetterPseudoElement(const RenderStyle& style)
{
auto display = style.display();
return display == DisplayType::Block
|| display == DisplayType::ListItem
|| display == DisplayType::InlineBlock
|| display == DisplayType::TableCell
|| display == DisplayType::TableCaption
|| display == DisplayType::FlowRoot;
};
std::optional<ElementUpdate> TreeResolver::resolvePseudoElement(Element& element, const PseudoElementIdentifier& pseudoElementIdentifier, const ElementUpdate& elementUpdate, IsInDisplayNoneTree isInDisplayNoneTree)
{
if (elementUpdate.style->display() == DisplayType::None)
return { };
if (pseudoElementIdentifier.pseudoId == PseudoId::Backdrop && !element.isInTopLayer())
return { };
if (pseudoElementIdentifier.pseudoId == PseudoId::Marker && elementUpdate.style->display() != DisplayType::ListItem)
return { };
if (pseudoElementIdentifier.pseudoId == PseudoId::FirstLine && !scope().resolver->usesFirstLineRules())
return { };
if (pseudoElementIdentifier.pseudoId == PseudoId::FirstLetter && !scope().resolver->usesFirstLetterRules())
return { };
if (pseudoElementIdentifier.pseudoId == PseudoId::WebKitScrollbar && elementUpdate.style->overflowX() != Overflow::Scroll && elementUpdate.style->overflowY() != Overflow::Scroll)
return { };
ASSERT_IMPLIES(pseudoElementIdentifier.pseudoId == PseudoId::ViewTransition
|| pseudoElementIdentifier.pseudoId == PseudoId::ViewTransitionGroup
|| pseudoElementIdentifier.pseudoId == PseudoId::ViewTransitionImagePair
|| pseudoElementIdentifier.pseudoId == PseudoId::ViewTransitionNew
|| pseudoElementIdentifier.pseudoId == PseudoId::ViewTransitionOld,
m_document->hasViewTransitionPseudoElementTree() && &element == m_document->documentElement());
if (!elementUpdate.style->hasPseudoStyle(pseudoElementIdentifier.pseudoId))
return resolveAncestorPseudoElement(element, pseudoElementIdentifier, elementUpdate);
if ((pseudoElementIdentifier.pseudoId == PseudoId::FirstLine || pseudoElementIdentifier.pseudoId == PseudoId::FirstLetter) && !supportsFirstLineAndLetterPseudoElement(*elementUpdate.style))
return { };
auto resolutionContext = makeResolutionContextForPseudoElement(elementUpdate, pseudoElementIdentifier);
auto resolvedStyle = scope().resolver->styleForPseudoElement(element, pseudoElementIdentifier, resolutionContext);
if (!resolvedStyle)
return { };
auto animatedUpdate = createAnimatedElementUpdate(WTFMove(*resolvedStyle), { element, pseudoElementIdentifier }, elementUpdate.changes, resolutionContext, isInDisplayNoneTree);
if (pseudoElementIdentifier.pseudoId == PseudoId::Before || pseudoElementIdentifier.pseudoId == PseudoId::After) {
if (scope().resolver->usesFirstLineRules()) {
// ::first-line can inherit to ::before/::after
if (auto firstLineContext = makeResolutionContextForInheritedFirstLine(elementUpdate, *elementUpdate.style)) {
auto firstLineStyle = scope().resolver->styleForPseudoElement(element, pseudoElementIdentifier, *firstLineContext);
firstLineStyle->style->setPseudoElementType(PseudoId::FirstLine);
animatedUpdate.style->addCachedPseudoStyle(WTFMove(firstLineStyle->style));
}
}
if (scope().resolver->usesFirstLetterRules()) {
auto beforeAfterContext = makeResolutionContextForPseudoElement(animatedUpdate, { PseudoId::FirstLetter });
if (auto firstLetterStyle = resolveAncestorFirstLetterPseudoElement(element, elementUpdate, beforeAfterContext))
animatedUpdate.style->addCachedPseudoStyle(WTFMove(firstLetterStyle->style));
}
}
return animatedUpdate;
}
std::optional<ElementUpdate> TreeResolver::resolveAncestorPseudoElement(Element& element, const PseudoElementIdentifier& pseudoElementIdentifier, const ElementUpdate& elementUpdate)
{
ASSERT(!elementUpdate.style->hasPseudoStyle(pseudoElementIdentifier.pseudoId));
auto pseudoElementStyle = [&]() -> std::optional<ResolvedStyle> {
// ::first-line and ::first-letter defined on an ancestor element may need to be resolved for the current element.
if (pseudoElementIdentifier.pseudoId == PseudoId::FirstLine)
return resolveAncestorFirstLinePseudoElement(element, elementUpdate);
if (pseudoElementIdentifier.pseudoId == PseudoId::FirstLetter) {
auto resolutionContext = makeResolutionContextForPseudoElement(elementUpdate, { PseudoId::FirstLetter });
return resolveAncestorFirstLetterPseudoElement(element, elementUpdate, resolutionContext);
}
return { };
}();
if (!pseudoElementStyle)
return { };
auto* oldStyle = element.renderOrDisplayContentsStyle(pseudoElementIdentifier);
auto changes = oldStyle ? determineChanges(*oldStyle, *pseudoElementStyle->style) : Change::Renderer;
auto resolutionContext = makeResolutionContextForPseudoElement(elementUpdate, pseudoElementIdentifier);
return createAnimatedElementUpdate(WTFMove(*pseudoElementStyle), { element, pseudoElementIdentifier }, changes, resolutionContext);
}
static bool isChildInBlockFormattingContext(const RenderStyle& style)
{
// FIXME: Incomplete. There should be shared code with layout for this.
if (style.display() != DisplayType::Block && style.display() != DisplayType::ListItem)
return false;
if (style.hasOutOfFlowPosition())
return false;
if (style.floating() != Float::None)
return false;
if (style.overflowX() != Overflow::Visible || style.overflowY() != Overflow::Visible)
return false;
return true;
};
std::optional<ResolvedStyle> TreeResolver::resolveAncestorFirstLinePseudoElement(Element& element, const ElementUpdate& elementUpdate)
{
if (elementUpdate.style->display() == DisplayType::Inline) {
auto* parent = boxGeneratingParent();
if (!parent)
return { };
auto resolutionContext = makeResolutionContextForInheritedFirstLine(elementUpdate, parent->style);
if (!resolutionContext)
return { };
auto elementStyle = scope().resolver->styleForElement(element, *resolutionContext);
elementStyle.style->setPseudoElementType(PseudoId::FirstLine);
return elementStyle;
}
auto findFirstLineElementForBlock = [&]() -> Element* {
if (!isChildInBlockFormattingContext(*elementUpdate.style))
return nullptr;
// ::first-line is only propagated to the first block.
if (parent().resolvedFirstLineAndLetterChild)
return nullptr;
for (auto& parent : makeReversedRange(m_parentStack)) {
if (parent.style.display() == DisplayType::Contents)
continue;
if (!supportsFirstLineAndLetterPseudoElement(parent.style))
return nullptr;
if (parent.style.hasPseudoStyle(PseudoId::FirstLine))
return parent.element;
if (!isChildInBlockFormattingContext(parent.style))
return nullptr;
}
return nullptr;
};
auto firstLineElement = findFirstLineElementForBlock();
if (!firstLineElement)
return { };
auto resolutionContext = makeResolutionContextForPseudoElement(elementUpdate, { PseudoId::FirstLine });
// Can't use the cached state since the element being resolved is not the current one.
resolutionContext.selectorMatchingState = nullptr;
return scope().resolver->styleForPseudoElement(*firstLineElement, { PseudoId::FirstLine }, resolutionContext);
}
std::optional<ResolvedStyle> TreeResolver::resolveAncestorFirstLetterPseudoElement(Element& element, const ElementUpdate& elementUpdate, ResolutionContext& resolutionContext)
{
auto findFirstLetterElement = [&]() -> Element* {
if (elementUpdate.style->hasPseudoStyle(PseudoId::FirstLetter) && supportsFirstLineAndLetterPseudoElement(*elementUpdate.style))
return &element;
// ::first-letter is only propagated to the first box.
if (parent().resolvedFirstLineAndLetterChild)
return nullptr;
bool skipInlines = elementUpdate.style->display() == DisplayType::Inline;
if (!skipInlines && !isChildInBlockFormattingContext(*elementUpdate.style))
return nullptr;
for (auto& parent : makeReversedRange(m_parentStack)) {
if (parent.style.display() == DisplayType::Contents)
continue;
if (skipInlines && parent.style.display() == DisplayType::Inline)
continue;
skipInlines = false;
if (!supportsFirstLineAndLetterPseudoElement(parent.style))
return nullptr;
if (parent.style.hasPseudoStyle(PseudoId::FirstLetter))
return parent.element;
if (!isChildInBlockFormattingContext(parent.style))
return nullptr;
}
return nullptr;
};
auto firstLetterElement = findFirstLetterElement();
if (!firstLetterElement)
return { };
// Can't use the cached state since the element being resolved is not the current one.
resolutionContext.selectorMatchingState = nullptr;
return scope().resolver->styleForPseudoElement(*firstLetterElement, { PseudoId::FirstLetter }, resolutionContext);
}
ResolutionContext TreeResolver::makeResolutionContext()
{
return {
&parent().style,
parentBoxStyle(),
documentElementStyle(),
&scope().selectorMatchingState,
&m_treeResolutionState
};
}
ResolutionContext TreeResolver::makeResolutionContextForPseudoElement(const ElementUpdate& elementUpdate, const PseudoElementIdentifier& pseudoElementIdentifier)
{
auto parentStyle = [&]() -> const RenderStyle* {
if (auto parentPseudoId = parentPseudoElement(pseudoElementIdentifier.pseudoId)) {
if (auto* parentPseudoStyle = elementUpdate.style->getCachedPseudoStyle({ *parentPseudoId, (*parentPseudoId == PseudoId::ViewTransitionGroup || *parentPseudoId == PseudoId::ViewTransitionImagePair) ? pseudoElementIdentifier.nameArgument : nullAtom() }))
return parentPseudoStyle;
}
return elementUpdate.style.get();
};
return {
parentStyle(),
parentBoxStyleForPseudoElement(elementUpdate),
documentElementStyle(),
&scope().selectorMatchingState,
&m_treeResolutionState
};
}
std::optional<ResolutionContext> TreeResolver::makeResolutionContextForInheritedFirstLine(const ElementUpdate& elementUpdate, const RenderStyle& inheritStyle)
{
auto parentFirstLineStyle = inheritStyle.getCachedPseudoStyle({ PseudoId::FirstLine });
if (!parentFirstLineStyle)
return { };
// First line style for inlines is made by inheriting from parent first line style.
return ResolutionContext {
parentFirstLineStyle,
parentBoxStyleForPseudoElement(elementUpdate),
documentElementStyle(),
&scope().selectorMatchingState,
&m_treeResolutionState
};
}
const RenderStyle* TreeResolver::documentElementStyle() const
{
if (m_computedDocumentElementStyle)
return m_computedDocumentElementStyle.get();
return m_document->documentElement()->renderStyle();
}
auto TreeResolver::boxGeneratingParent() const -> const Parent*
{
// 'display: contents' doesn't generate boxes.
for (auto& parent : makeReversedRange(m_parentStack)) {
if (parent.style.display() == DisplayType::None)
return nullptr;
if (parent.style.display() != DisplayType::Contents)
return &parent;
}
ASSERT_NOT_REACHED();
return nullptr;
}
const RenderStyle* TreeResolver::parentBoxStyle() const
{
auto* parent = boxGeneratingParent();
return parent ? &parent->style : nullptr;
}
const RenderStyle* TreeResolver::parentBoxStyleForPseudoElement(const ElementUpdate& elementUpdate) const
{
switch (elementUpdate.style->display()) {
case DisplayType::None:
return nullptr;
case DisplayType::Contents:
return parentBoxStyle();
default:
return elementUpdate.style.get();
}
}
ElementUpdate TreeResolver::createAnimatedElementUpdate(ResolvedStyle&& resolvedStyle, const Styleable& styleable, OptionSet<Change> parentChanges, const ResolutionContext& resolutionContext, IsInDisplayNoneTree isInDisplayNoneTree)
{
auto& element = styleable.element;
auto& document = element.document();
auto* currentStyle = element.renderOrDisplayContentsStyle(styleable.pseudoElementIdentifier);
std::unique_ptr<RenderStyle> startingStyle;
auto* oldStyle = [&]() -> const RenderStyle* {
if (auto* styleBefore = beforeResolutionStyle(element, styleable.pseudoElementIdentifier))
return styleBefore;
if (resolvedStyle.style->hasTransitions()) {
// https://drafts.csswg.org/css-transitions-2/#at-ruledef-starting-style
// "If an element does not have a before-change style for a given style change event, the starting style is used instead."
startingStyle = resolveStartingStyle(resolvedStyle, styleable, resolutionContext);
return startingStyle.get();
}
return nullptr;
}();
auto unanimatedDisplay = resolvedStyle.style->display();
auto hasUnresolvedAnchorPosition = this->hasUnresolvedAnchorPosition(styleable);
WeakStyleOriginatedAnimations newStyleOriginatedAnimations;
auto updateAnimations = [&] {
if (document.backForwardCacheState() != Document::NotInBackForwardCache || document.printing())
return;
if (hasUnresolvedAnchorPosition)
return;
if (oldStyle && (oldStyle->hasTransitions() || resolvedStyle.style->hasTransitions()))
styleable.updateCSSTransitions(*oldStyle, *resolvedStyle.style, newStyleOriginatedAnimations);
if ((oldStyle && oldStyle->hasScrollTimelines()) || resolvedStyle.style->hasScrollTimelines())
styleable.updateCSSScrollTimelines(oldStyle, *resolvedStyle.style);
if ((oldStyle && oldStyle->hasViewTimelines()) || resolvedStyle.style->hasViewTimelines())
styleable.updateCSSViewTimelines(oldStyle, *resolvedStyle.style);
if ((oldStyle && oldStyle->timelineScope().type != NameScope::Type::None) || resolvedStyle.style->timelineScope().type != NameScope::Type::None) {
CheckedRef styleOriginatedTimelinesController = element.protectedDocument()->ensureStyleOriginatedTimelinesController();
styleOriginatedTimelinesController->updateNamedTimelineMapForTimelineScope(resolvedStyle.style->timelineScope(), styleable);
}
// The order in which CSS Transitions and CSS Animations are updated matters since CSS Transitions define the after-change style
// to use CSS Animations as defined in the previous style change event. As such, we update CSS Animations after CSS Transitions
// such that when CSS Transitions are updated the CSS Animations data is the same as during the previous style change event.
if ((oldStyle && oldStyle->hasAnimations()) || resolvedStyle.style->hasAnimations())
styleable.updateCSSAnimations(oldStyle, *resolvedStyle.style, resolutionContext, newStyleOriginatedAnimations, isInDisplayNoneTree);
};
auto applyAnimations = [&]() -> std::pair<std::unique_ptr<RenderStyle>, OptionSet<AnimationImpact>> {
if (hasUnresolvedAnchorPosition)
return { WTFMove(resolvedStyle.style), OptionSet<AnimationImpact> { } };
if (!styleable.hasKeyframeEffects()) {
// FIXME: Push after-change style into parent stack instead.
styleable.setLastStyleChangeEventStyle(resolveAfterChangeStyleForNonAnimated(resolvedStyle, styleable, resolutionContext));
styleable.setHasPropertiesOverridenAfterAnimation(false);
return { WTFMove(resolvedStyle.style), OptionSet<AnimationImpact> { } };
}
auto previousLastStyleChangeEventStyle = styleable.lastStyleChangeEventStyle() ? RenderStyle::clonePtr(*styleable.lastStyleChangeEventStyle()) : nullptr;
// Record the style prior to applying animations for this style change event.
styleable.setLastStyleChangeEventStyle(RenderStyle::clonePtr(*resolvedStyle.style));
// Apply all keyframe effects to the new style.
HashSet<AnimatableCSSProperty> animatedProperties;
auto animatedStyle = RenderStyle::clonePtr(*resolvedStyle.style);
auto animationImpact = styleable.applyKeyframeEffects(*animatedStyle, animatedProperties, previousLastStyleChangeEventStyle.get(), resolutionContext);
if (*resolvedStyle.style == *animatedStyle && animationImpact.isEmpty() && previousLastStyleChangeEventStyle)
return { WTFMove(resolvedStyle.style), animationImpact };
if (resolvedStyle.matchResult) {
auto animatedStyleBeforeCascadeApplication = RenderStyle::clonePtr(*animatedStyle);
// The cascade may override animated properties and have dependencies to them.
// FIXME: This is wrong if there are both transitions and animations running on the same element.
auto overriddenAnimatedProperties = applyCascadeAfterAnimation(*animatedStyle, animatedProperties, styleable.hasRunningTransitions(), *resolvedStyle.matchResult, element, resolutionContext);
ASSERT(styleable.keyframeEffectStack());
styleable.keyframeEffectStack()->cascadeDidOverrideProperties(overriddenAnimatedProperties, document);
styleable.setHasPropertiesOverridenAfterAnimation(!overriddenAnimatedProperties.isEmpty());
}
Adjuster adjuster(document, *resolutionContext.parentStyle, resolutionContext.parentBoxStyle, !styleable.pseudoElementIdentifier ? &styleable.element : nullptr);
adjuster.adjustAnimatedStyle(*animatedStyle, animationImpact);
return { WTFMove(animatedStyle), animationImpact };
};
// FIXME: Something like this is also needed for viewport units.
if (currentStyle && parent().needsUpdateQueryContainerDependentStyle)
styleable.queryContainerDidChange();
// First, we need to make sure that any new CSS animation occuring on this element has a matching WebAnimation
// on the document timeline.
updateAnimations();
// Now we can update all Web animations, which will include CSS Animations as well
// as animations created via the JS API.
auto [newStyle, animationImpact] = applyAnimations();
// Deduplication speeds up equality comparisons as the properties inherit to descendants.
// FIXME: There should be a more general mechanism for this.
if (currentStyle)
newStyle->deduplicateCustomProperties(*currentStyle);
auto changes = currentStyle ? determineChanges(*currentStyle, *newStyle) : Change::Renderer;
if (element.hasInvalidRenderer() || parentChanges.contains(Change::Renderer))
changes.add(Change::Renderer);
collectChangedAnchorNames(*newStyle, currentStyle);
auto animationsAffectedDisplay = [&, animatedDisplay = newStyle->display()]() {
auto* keyframeEffectStack = styleable.keyframeEffectStack();
if (!keyframeEffectStack)
return false;
if (unanimatedDisplay != animatedDisplay)
return true;
return keyframeEffectStack->containsProperty(CSSPropertyDisplay);
}();
if (!affectsRenderedSubtree(styleable.element, *newStyle) && !animationsAffectedDisplay) {
// If after updating animations we end up not rendering this element or its subtree
// and the update did not change the "display" value then we should cancel all
// style-originated animations while ensuring that the new ones are canceled silently,
// as if they hadn't been created.
styleable.cancelStyleOriginatedAnimations(newStyleOriginatedAnimations);
} else if (!newStyleOriginatedAnimations.isEmpty()) {
// If style-originated animations were not canceled, then we should make sure that
// the creation of new style-originated animations during this update is known to the
// document's timeline as animation scheduling was paused for any animation created
// during this update.
if (auto* timeline = m_document->existingTimeline())
timeline->styleOriginatedAnimationsWereCreated();
}
if (animationsAffectedDisplay)
newStyle->setHasDisplayAffectedByAnimations();
bool shouldRecompositeLayer = animationImpact.contains(AnimationImpact::RequiresRecomposite) || element.styleResolutionShouldRecompositeLayer();
auto mayNeedRebuildRoot = [&, newStyle = newStyle.get()] {
if (changes.contains(Change::Renderer))
return true;
// We may need to rebuild the tree starting from further up if there is a position property change to clean up continuations.
if (currentStyle && currentStyle->position() != newStyle->position())
return true;
return false;
}();
return { WTFMove(newStyle), changes, shouldRecompositeLayer, mayNeedRebuildRoot };
}
std::unique_ptr<RenderStyle> TreeResolver::resolveStartingStyle(const ResolvedStyle& resolvedStyle, const Styleable& styleable, const ResolutionContext& resolutionContext)
{
if (!resolvedStyle.matchResult || !resolvedStyle.matchResult->hasStartingStyle)
return nullptr;
// "Starting style inherits from the parent’s after-change style just like after-change style does."
auto& parentStyle = parentAfterChangeStyle(styleable, resolutionContext);
// We now resolve the starting style by applying all rules (including @starting-style ones) again.
// We could compute it along with the primary style and include it in MatchedPropertiesCache but it is not
// clear this would be benefitial as it is typically only used once.
return resolveAgainInDifferentContext(resolvedStyle, styleable, parentStyle, PropertyCascade::startingStylePropertyTypes(), { }, resolutionContext);
}
std::unique_ptr<RenderStyle> TreeResolver::resolveAfterChangeStyleForNonAnimated(const ResolvedStyle& resolvedStyle, const Styleable& styleable, const ResolutionContext& resolutionContext)
{
// Element may have after-change style differing from the current style in case they are inheriting from a transitioning element.
// We need after-change style for non-animating elements only in case there @starting-style rules in the subtree.
if (!scope().resolver->usesStartingStyleRules())
return nullptr;
if (!resolvedStyle.matchResult)
return nullptr;
if (styleable.pseudoElementIdentifier)
return nullptr;
if (!parent().element || !parent().element->lastStyleChangeEventStyle({ }))
return nullptr;
// "Likewise, define the after-change style as.. and inheriting from the after-change style of the parent."
auto& parentStyle = parentAfterChangeStyle(styleable, resolutionContext);
return resolveAgainInDifferentContext(resolvedStyle, styleable, parentStyle, PropertyCascade::normalPropertyTypes(), { }, resolutionContext);
}
std::unique_ptr<RenderStyle> TreeResolver::resolveAgainInDifferentContext(const ResolvedStyle& resolvedStyle, const Styleable& styleable, const RenderStyle& parentStyle, OptionSet<PropertyCascade::PropertyType> properties, std::optional<BuilderPositionTryFallback>&& positionTryFallback, const ResolutionContext& resolutionContext)
{
ASSERT(resolvedStyle.matchResult);
auto newStyle = RenderStyle::createPtr();
newStyle->inheritFrom(parentStyle);
if (styleable.pseudoElementIdentifier)
newStyle->setPseudoElementType(styleable.pseudoElementIdentifier->pseudoId);
auto builderContext = BuilderContext {
m_document.get(),
&parentStyle,
resolutionContext.documentElementStyle,
&styleable.element,
&m_treeResolutionState,
WTFMove(positionTryFallback)
};
auto styleBuilder = Builder {
*newStyle,
WTFMove(builderContext),
*resolvedStyle.matchResult,
CascadeLevel::Author,
{ properties }
};
styleBuilder.applyAllProperties();
if (newStyle->display() == DisplayType::None)
return nullptr;
Adjuster adjuster(m_document, parentStyle, resolutionContext.parentBoxStyle, !styleable.pseudoElementIdentifier ? &styleable.element : nullptr);
adjuster.adjust(*newStyle);
return newStyle;
}
const RenderStyle& TreeResolver::parentAfterChangeStyle(const Styleable& styleable, const ResolutionContext& resolutionContext) const
{
if (auto* parentElement = !styleable.pseudoElementIdentifier ? parent().element : &styleable.element) {
if (auto* afterChangeStyle = parentElement->lastStyleChangeEventStyle({ }))
return *afterChangeStyle;
}
return *resolutionContext.parentStyle;
}
HashSet<AnimatableCSSProperty> TreeResolver::applyCascadeAfterAnimation(RenderStyle& animatedStyle, const HashSet<AnimatableCSSProperty>& animatedProperties, bool isTransition, const MatchResult& matchResult, const Element& element, const ResolutionContext& resolutionContext)
{
auto builderContext = BuilderContext {
m_document.get(),
resolutionContext.parentStyle,
resolutionContext.documentElementStyle,
&element,
&m_treeResolutionState
};
auto styleBuilder = Builder {
animatedStyle,
WTFMove(builderContext),
matchResult,
CascadeLevel::Author,
{ isTransition ? PropertyCascade::PropertyType::AfterTransition : PropertyCascade::PropertyType::AfterAnimation },
&animatedProperties
};
styleBuilder.applyAllProperties();
return styleBuilder.overriddenAnimatedProperties();
}
#if ENABLE(ACCESSIBILITY_ISOLATED_TREE)
void TreeResolver::pushParent(Element& element, const RenderStyle& style, OptionSet<Change> changes, DescendantsToResolve descendantsToResolve, IsInDisplayNoneTree isInDisplayNoneTree, bool didAXUpdateFontSubtree, bool didAXUpdateTextColorSubtree)
#else
void TreeResolver::pushParent(Element& element, const RenderStyle& style, OptionSet<Change> changes, DescendantsToResolve descendantsToResolve, IsInDisplayNoneTree isInDisplayNoneTree)
#endif
{
scope().selectorMatchingState.selectorFilter.pushParent(&element);
if (style.containerType() != ContainerType::Normal)
scope().selectorMatchingState.containerQueryEvaluationState.sizeQueryContainers.append(element);
Parent parent(element, style, changes, descendantsToResolve, isInDisplayNoneTree);
#if ENABLE(ACCESSIBILITY_ISOLATED_TREE)
parent.didAXUpdateFontSubtree = didAXUpdateFontSubtree;
parent.didAXUpdateTextColorSubtree = didAXUpdateTextColorSubtree;
#endif
if (auto* shadowRoot = element.shadowRoot()) {
pushScope(*shadowRoot);
parent.didPushScope = true;
} else if (RefPtr slot = dynamicDowncast<HTMLSlotElement>(element); slot && slot->assignedNodes()) {
pushEnclosingScope();
parent.didPushScope = true;
}
parent.needsUpdateQueryContainerDependentStyle = m_parentStack.last().needsUpdateQueryContainerDependentStyle || element.needsUpdateQueryContainerDependentStyle();
element.clearNeedsUpdateQueryContainerDependentStyle();
m_parentStack.append(WTFMove(parent));
}
void TreeResolver::popParent()
{
auto& parentElement = *parent().element;
parentElement.setHasValidStyle();
parentElement.clearChildNeedsStyleRecalc();
// FIXME: Push after-change style into parent stack instead.
if (!parentElement.hasKeyframeEffects({ }))
parentElement.setLastStyleChangeEventStyle({ }, nullptr);
if (parent().didPushScope)
popScope();
scope().selectorMatchingState.selectorFilter.popParent();
auto& queryContainers = scope().selectorMatchingState.containerQueryEvaluationState.sizeQueryContainers;
if (!queryContainers.isEmpty() && queryContainers.last().ptr() == &parentElement)
queryContainers.removeLast();
m_parentStack.removeLast();
}
void TreeResolver::popParentsToDepth(unsigned depth)
{
ASSERT(depth);
ASSERT(m_parentStack.size() >= depth);
while (m_parentStack.size() > depth)
popParent();
}
auto TreeResolver::determineResolutionType(const Element& element, const RenderStyle* existingStyle, DescendantsToResolve parentDescendantsToResolve, OptionSet<Change> parentChanges) -> std::optional<ResolutionType>
{
auto combinedValidity = [&] {
auto validity = element.styleValidity();
if (auto* pseudoElement = element.beforePseudoElement())
validity = std::max(validity, pseudoElement->styleValidity());
if (auto* pseudoElement = element.afterPseudoElement())
validity = std::max(validity, pseudoElement->styleValidity());
return validity;
}();
if (parentDescendantsToResolve == DescendantsToResolve::None) {
if (combinedValidity == Validity::AnimationInvalid)
return ResolutionType::AnimationOnly;
if (combinedValidity == Validity::Valid && element.hasInvalidRenderer())
return existingStyle ? ResolutionType::RebuildUsingExisting : ResolutionType::Full;
if (combinedValidity == Validity::InlineStyleInvalid && existingStyle)
return ResolutionType::FullWithMatchResultCache;
}
if (combinedValidity > Validity::Valid)
return ResolutionType::Full;
switch (parentDescendantsToResolve) {
case DescendantsToResolve::None:
return { };
case DescendantsToResolve::RebuildAllUsingExisting:
return existingStyle ? ResolutionType::RebuildUsingExisting : ResolutionType::Full;
case DescendantsToResolve::Children: {
auto canFastPathInherit = [&] {
if (parentChanges.contains(Change::Inherited))
return false;
if (!parentChanges.contains(Change::FastPathInherited))
return false;
if (!existingStyle || existingStyle->disallowsFastPathInheritance())
return false;
// Some non-inherited property changed along with a fast-path property and we may need to inherit it too.
if (parentChanges.contains(Change::NonInherited) && existingStyle->hasExplicitlyInheritedProperties())
return false;
return true;
};
return canFastPathInherit() ? ResolutionType::FastPathInherit : ResolutionType::Full;
}
case DescendantsToResolve::All:
return ResolutionType::Full;
case DescendantsToResolve::ChildrenWithExplicitInherit:
if (!existingStyle || existingStyle->hasExplicitlyInheritedProperties())
return ResolutionType::Full;
return { };
};
ASSERT_NOT_REACHED();
return { };
}
static void clearNeedsStyleResolution(Element& element)
{
element.setHasValidStyle();
if (auto* before = element.beforePseudoElement())
before->setHasValidStyle();
if (auto* after = element.afterPseudoElement())
after->setHasValidStyle();
}
static bool hasLoadingStylesheet(const Style::Scope& styleScope, const Element& element, bool checkDescendants)
{
if (!styleScope.hasPendingSheetsInBody())
return false;
if (styleScope.hasPendingSheetInBody(element))
return true;
if (!checkDescendants)
return false;
for (auto& descendant : descendantsOfType<Element>(element)) {
if (styleScope.hasPendingSheetInBody(descendant))
return true;
};
return false;
}
static std::unique_ptr<RenderStyle> createInheritedDisplayContentsStyleIfNeeded(const RenderStyle& parentElementStyle, const RenderStyle* parentBoxStyle)
{
if (parentElementStyle.display() != DisplayType::Contents)
return nullptr;
if (parentBoxStyle && parentBoxStyle->inheritedEqual(parentElementStyle))
return nullptr;
// Compute style for imaginary unstyled <span> around the text node.
auto style = RenderStyle::createPtr();
style->inheritFrom(parentElementStyle);
return style;
}
void TreeResolver::resetDescendantStyleRelations(Element& element, DescendantsToResolve descendantsToResolve)
{
switch (descendantsToResolve) {
case DescendantsToResolve::None:
case DescendantsToResolve::RebuildAllUsingExisting:
case DescendantsToResolve::ChildrenWithExplicitInherit:
break;
case DescendantsToResolve::Children:
element.resetChildStyleRelations();
break;
case DescendantsToResolve::All:
element.resetAllDescendantStyleRelations();
break;
};
}
void TreeResolver::resolveComposedTree()
{
ASSERT(m_parentStack.size() == 1);
ASSERT(m_scopeStack.size() == 1);
auto descendants = composedTreeDescendants(m_document);
auto it = descendants.begin();
auto end = descendants.end();
while (it != end) {
popParentsToDepth(it.depth());
auto& node = *it;
auto& parent = this->parent();
ASSERT(node.isConnected());
ASSERT(node.containingShadowRoot() == scope().shadowRoot);
ASSERT(node.parentElement() == parent.element || is<ShadowRoot>(node.parentNode()) || node.parentElement()->shadowRoot());
if (RefPtr text = dynamicDowncast<Text>(node)) {
auto containsOnlyASCIIWhitespace = text->containsOnlyASCIIWhitespace();
auto needsTextUpdate = [&] {
if ((text->hasInvalidRenderer() && parent.changes != Change::Renderer) || parent.style.display() == DisplayType::Contents)
return true;
if (!text->renderer() && containsOnlyASCIIWhitespace && parent.style.preserveNewline()) {
// FIXME: This really needs to be done only when parent.style.preserveNewline() changes value.
return true;
}
return false;
};
if (needsTextUpdate()) {
TextUpdate textUpdate;
textUpdate.inheritedDisplayContentsStyle = createInheritedDisplayContentsStyleIfNeeded(parent.style, parentBoxStyle());
m_update->addText(*text, parent.element, WTFMove(textUpdate));
}
if (!containsOnlyASCIIWhitespace)
parent.resolvedFirstLineAndLetterChild = true;
text->setHasValidStyle();
it.traverseNextSkippingChildren();
continue;
}
auto& element = downcast<Element>(node);
if (it.depth() > Settings::defaultMaximumRenderTreeDepth) {
resetStyleForNonRenderedDescendants(element);
it.traverseNextSkippingChildren();
continue;
}
auto* style = existingStyle(element);
auto changes = OptionSet<Change> { };
auto descendantsToResolve = DescendantsToResolve::None;
#if ENABLE(ACCESSIBILITY_ISOLATED_TREE)
bool didAXUpdateFontSubtree = parent.didAXUpdateFontSubtree;
bool didAXUpdateTextColorSubtree = parent.didAXUpdateTextColorSubtree;
#endif
auto resolutionType = determineResolutionType(element, style, parent.descendantsToResolve, parent.changes);
if (resolutionType) {
element.resetComputedStyle();
if (*resolutionType == ResolutionType::Full)
element.resetStyleRelations();
if (element.hasCustomStyleResolveCallbacks())
element.willRecalcStyle(parent.changes);
auto [elementUpdate, elementDescendantsToResolve] = resolveElement(element, style, *resolutionType);
if (element.hasCustomStyleResolveCallbacks())
element.didRecalcStyle(elementUpdate.changes);
if (CheckedPtr cache = m_document->existingAXObjectCache()) {
cache->onStyleChange(element, elementUpdate.changes, style, elementUpdate.style.get());
#if ENABLE(ACCESSIBILITY_ISOLATED_TREE)
if (!didAXUpdateFontSubtree)
didAXUpdateFontSubtree = cache->onFontChange(element, style, elementUpdate.style.get());
if (!didAXUpdateTextColorSubtree)
didAXUpdateTextColorSubtree = cache->onTextColorChange(element, style, elementUpdate.style.get());
#endif // ENABLE(ACCESSIBILITY_ISOLATED_TREE)
}
style = elementUpdate.style.get();
changes = elementUpdate.changes;
descendantsToResolve = elementDescendantsToResolve;
if (style || element.hasDisplayNone())
m_update->addElement(element, parent.element, WTFMove(elementUpdate));
if (style && &element == m_document->documentElement())
m_computedDocumentElementStyle = RenderStyle::clonePtr(*style);
clearNeedsStyleResolution(element);
}
if (!style)
resetStyleForNonRenderedDescendants(element);
auto queryContainerAction = updateStateForQueryContainer(element, style, descendantsToResolve);
auto anchorPositionedElementAction = updateAnchorPositioningState(element, style);
resumeDescendantResolutionIfNeeded(element, changes, descendantsToResolve);
bool shouldIterateChildren = [&] {
// display::none, no need to resolve descendants.
if (!style)
return false;
// Style resolution will be resumed after the container or anchor-positioned element has been resolved.
if (queryContainerAction == LayoutInterleavingAction::SkipDescendants || anchorPositionedElementAction == LayoutInterleavingAction::SkipDescendants) {
deferDescendantResolution(element, changes, descendantsToResolve);
return false;
}
return element.childNeedsStyleRecalc() || descendantsToResolve != DescendantsToResolve::None;
}();
if (!m_didSeePendingStylesheet)
m_didSeePendingStylesheet = hasLoadingStylesheet(m_document->styleScope(), element, !shouldIterateChildren);
if (!parent.resolvedFirstLineAndLetterChild && style && generatesBox(*style) && supportsFirstLineAndLetterPseudoElement(*style))
parent.resolvedFirstLineAndLetterChild = true;
if (!shouldIterateChildren) {
it.traverseNextSkippingChildren();
continue;
}
resetDescendantStyleRelations(element, descendantsToResolve);
auto isInDisplayNoneTree = parent.isInDisplayNoneTree == IsInDisplayNoneTree::Yes || !style || style->display() == DisplayType::None;
#if ENABLE(ACCESSIBILITY_ISOLATED_TREE)
pushParent(element, *style, changes, descendantsToResolve, isInDisplayNoneTree ? IsInDisplayNoneTree::Yes : IsInDisplayNoneTree::No, didAXUpdateFontSubtree, didAXUpdateTextColorSubtree);
#else
pushParent(element, *style, changes, descendantsToResolve, isInDisplayNoneTree ? IsInDisplayNoneTree::Yes : IsInDisplayNoneTree::No);
#endif
it.traverseNext();
}
popParentsToDepth(1);
}
const RenderStyle* TreeResolver::existingStyle(const Element& element)
{
auto* style = element.renderOrDisplayContentsStyle();
if (style && &element == m_document->documentElement()) {
// Document element style may have got adjusted based on body style but we don't want to inherit those adjustments.
m_computedDocumentElementStyle = Adjuster::restoreUsedDocumentElementStyleToComputed(*style);
if (m_computedDocumentElementStyle)
style = m_computedDocumentElementStyle.get();
}
return style;
}
void TreeResolver::deferDescendantResolution(Element& element, OptionSet<Change> changes, DescendantsToResolve descendantsToResolve)
{
m_deferredDescendantResolutionStates.add(element, DeferredDescendantResolutionState {
.changes = changes,
.descendantsToResolve = descendantsToResolve
});
}
void TreeResolver::resumeDescendantResolutionIfNeeded(Element& element, OptionSet<Change>& changes, DescendantsToResolve& descendantsToResolve)
{
auto it = m_deferredDescendantResolutionStates.find(element);
if (it == m_deferredDescendantResolutionStates.end())
return;
const auto& state = it->value;
changes |= state.changes;
descendantsToResolve = std::max(descendantsToResolve, state.descendantsToResolve);
m_deferredDescendantResolutionStates.remove(it);
}
auto TreeResolver::updateStateForQueryContainer(Element& element, const RenderStyle* style, DescendantsToResolve& descendantsToResolve) -> LayoutInterleavingAction
{
if (!style)
return LayoutInterleavingAction::None;
if (m_queryContainerStates.contains(element))
return LayoutInterleavingAction::None;
auto* existingStyle = element.renderOrDisplayContentsStyle();
if (style->containerType() != ContainerType::Normal || (existingStyle && existingStyle->containerType() != ContainerType::Normal)) {
// If any of the queries use font-size relative units then a font size change
// may affect their evaluation, so force re-evaluating all descendants.
if (styleChangeAffectsRelativeUnits(*style, existingStyle))
descendantsToResolve = DescendantsToResolve::All;
m_queryContainerStates.add(element, QueryContainerState { });
return LayoutInterleavingAction::SkipDescendants;
}
return LayoutInterleavingAction::None;
}
std::unique_ptr<Update> TreeResolver::resolve()
{
auto didInterleavedLayout = std::exchange(m_needsInterleavedLayout, false);
if (didInterleavedLayout)
m_didFirstInterleavedLayout = true;
Element* documentElement = m_document->documentElement();
if (!documentElement) {
m_document->styleScope().resolver();
return nullptr;
}
if (didInterleavedLayout)
AnchorPositionEvaluator::updateAnchorPositioningStatesAfterInterleavedLayout(m_document, m_treeResolutionState.anchorPositionedStates);
if (!documentElement->childNeedsStyleRecalc() && !documentElement->needsStyleRecalc())
return WTFMove(m_update);
m_didSeePendingStylesheet = m_document->styleScope().hasPendingSheetsBeforeBody();
if (!m_update)
m_update = makeUnique<Update>(m_document);
m_scopeStack.append(adoptRef(*new Scope(m_document, *m_update)));
m_parentStack.append(Parent(m_document));
resolveComposedTree();
ASSERT(m_scopeStack.size() == 1);
ASSERT(m_parentStack.size() == 1);
m_parentStack.clear();
popScope();
for (auto& [element, state] : m_queryContainerStates) {
// Ensure that resumed resolution reaches the container.
if (!state.invalidated) {
element->invalidateForResumingQueryContainerResolution();
state.invalidated = true;
m_needsInterleavedLayout = true;
}
}
for (auto& elementAndState : m_treeResolutionState.anchorPositionedStates) {
// Ensure that style resolution visits any unresolved anchor-positioned elements.
if (elementAndState.value->stage < AnchorPositionResolutionStage::Resolved) {
const_cast<Element&>(*elementAndState.key.first).invalidateForResumingAnchorPositionedElementResolution();
m_needsInterleavedLayout = true;
saveBeforeResolutionStyleForInterleaving(*elementAndState.key.first);
}
}
for (auto& elementAndOptions : m_positionOptions) {
if (!elementAndOptions.value.chosen) {
elementAndOptions.key->invalidateForResumingAnchorPositionedElementResolution();
m_needsInterleavedLayout = true;
saveBeforeResolutionStyleForInterleaving(elementAndOptions.key);
}
}
if (!m_changedAnchorNames.isEmpty() || m_allAnchorNamesInvalid) {
// If there are changes to the anchor names then loop through the existing anchors and see if any of them references those names.
for (auto entry : m_document->styleScope().anchorPositionedToAnchorMap()) {
CheckedRef anchorPositionedElement = entry.key;
auto& anchors = entry.value;
bool anchorPositionedReferencesChangedAnchorNames = [&] {
if (m_allAnchorNamesInvalid)
return true;
for (auto anchor : anchors.anchors) {
if (m_changedAnchorNames.contains(anchor.name.name()))
return true;
}
return false;
}();
if (anchorPositionedReferencesChangedAnchorNames) {
// Invalidate the anchor-positioned element, so subsequent style resolution rounds would visit it.
anchorPositionedElement->invalidateForResumingAnchorPositionedElementResolution();
// Mark that additional style resolution round is needed.
m_needsInterleavedLayout = true;
// If the anchor-positioned element is currently being tracked for resolution,
// reset the resolution stage to FindAnchor. This re-runs anchor resolution to
// pick up new anchor name changes.
AnchorPositionedKey anchorPositionedKey { anchorPositionedElement.ptr(), anchors.pseudoElementIdentifier };
auto stateIt = m_treeResolutionState.anchorPositionedStates.find(anchorPositionedKey);
if (stateIt != m_treeResolutionState.anchorPositionedStates.end()) {
ASSERT(stateIt->value);
stateIt->value->stage = AnchorPositionResolutionStage::FindAnchors;
}
}
}
m_changedAnchorNames = { };
m_allAnchorNamesInvalid = false;
}
if (m_update->roots().isEmpty())
return { };
// Ensure we do at least one interleaved layout as any style change may affect existing anchor positions.
if (!m_didFirstInterleavedLayout && !m_document->styleScope().anchorPositionedToAnchorMap().isEmptyIgnoringNullReferences())
m_needsInterleavedLayout = true;
Adjuster::propagateToDocumentElementAndInitialContainingBlock(*m_update, m_document);
return WTFMove(m_update);
}
auto TreeResolver::updateAnchorPositioningState(Element& element, const RenderStyle* style) -> LayoutInterleavingAction
{
if (!style)
return LayoutInterleavingAction::None;
auto update = [&](const RenderStyle* style) {
if (!style)
return;
AnchorPositionEvaluator::updateAnchorPositionedStateForDefaultAnchor(element, *style, m_treeResolutionState.anchorPositionedStates);
};
update(style);
update(style->getCachedPseudoStyle({ PseudoId::Before }));
update(style->getCachedPseudoStyle({ PseudoId::After }));
auto needsInterleavedLayout = hasUnresolvedAnchorPosition({ element, { } });
if (needsInterleavedLayout)
return LayoutInterleavingAction::SkipDescendants;
return LayoutInterleavingAction::None;
}
void TreeResolver::generatePositionOptionsIfNeeded(const ResolvedStyle& resolvedStyle, const Styleable& styleable, const ResolutionContext& resolutionContext)
{
// https://drafts.csswg.org/css-anchor-position-1/#fallback-apply
if (!resolvedStyle.style || resolvedStyle.style->positionTryFallbacks().isEmpty())
return;
if (!resolvedStyle.style->hasOutOfFlowPosition())
return;
if (m_positionOptions.contains(styleable.element))
return;
auto generatePositionOptions = [&] {
auto options = PositionOptions { .originalStyle = RenderStyle::clonePtr(*resolvedStyle.style) };
options.optionStyles.reserveInitialCapacity(resolvedStyle.style->positionTryFallbacks().size());
for (auto& fallback : resolvedStyle.style->positionTryFallbacks()) {
auto optionStyle = generatePositionOption(fallback, resolvedStyle, styleable, resolutionContext);
if (!optionStyle)
continue;
options.optionStyles.append(WTFMove(optionStyle));
}
return options;
};
auto options = generatePositionOptions();
// If the fallbacks contain anchor references we need to resolve the anchors first and regenerate the options.
if (hasUnresolvedAnchorPosition(styleable))
return;
m_positionOptions.add(styleable.element, WTFMove(options));
}
std::unique_ptr<RenderStyle> TreeResolver::generatePositionOption(const PositionTryFallback& fallback, const ResolvedStyle& resolvedStyle, const Styleable& styleable, const ResolutionContext& resolutionContext)
{
// https://drafts.csswg.org/css-anchor-position-1/#fallback-apply
if (!resolvedStyle.matchResult)
return { };
auto resolveFallbackProperties = [&]() -> RefPtr<const StyleProperties> {
if (fallback.positionAreaProperties) {
ASSERT(!fallback.positionTryRuleName);
ASSERT(fallback.tactics.isEmpty());
return fallback.positionAreaProperties;
}
if (!fallback.positionTryRuleName)
return nullptr;
auto* styleScope = Style::Scope::forOrdinal(styleable.element, fallback.positionTryRuleName->scopeOrdinal);
if (!styleScope)
return nullptr;
auto& ruleSet = styleScope->resolver().ruleSets().authorStyle();
auto rule = ruleSet.positionTryRuleForName(fallback.positionTryRuleName->name);
if (!rule)
return nullptr;
return rule->properties();
};
auto builderFallback = BuilderPositionTryFallback {
.properties = resolveFallbackProperties(),
.tactics = fallback.tactics
};
return resolveAgainInDifferentContext(resolvedStyle, styleable, *resolutionContext.parentStyle, PropertyCascade::normalPropertyTypes(), WTFMove(builderFallback), resolutionContext);
}
void TreeResolver::sortPositionOptionsIfNeeded(PositionOptions& options, const Styleable& styleable)
{
if (options.sorted)
return;
options.sorted = true;
auto order = options.originalStyle->positionTryOrder();
if (order == PositionTryOrder::Normal || options.optionStyles.size() < 2)
return;
CheckedPtr box = dynamicDowncast<RenderBox>(styleable.renderer());
if (!box || !box->isOutOfFlowPositioned())
return;
// "For each entry in the position options list, apply that position option to the box, and find
// the specified inset-modified containing block size that results from those styles."
// https://drafts.csswg.org/css-anchor-position-1/#position-try-order-property
auto boxAxis = boxAxisForPositionTryOrder(order, options.originalStyle->writingMode());
struct SortingOption {
std::unique_ptr<RenderStyle> style;
LayoutUnit containingBlockSize;
};
Vector<SortingOption> optionsForSorting;
optionsForSorting.reserveInitialCapacity(options.optionStyles.size());
for (auto& optionStyle : options.optionStyles) {
auto constraints = PositionedLayoutConstraints { *box, *optionStyle, boxAxis };
constraints.computeInsets();
optionsForSorting.append({ WTFMove(optionStyle), constraints.insetModifiedContainingSize() });
}
// "Stably sort the position options list according to this size, with the largest coming first."
std::ranges::stable_sort(optionsForSorting, std::ranges::greater { }, &SortingOption::containingBlockSize);
for (size_t i = 0; i < optionsForSorting.size(); ++i)
options.optionStyles[i] = WTFMove(optionsForSorting[i].style);
}
std::optional<ResolvedStyle> TreeResolver::tryChoosePositionOption(const Styleable& styleable, const RenderStyle* existingStyle)
{
// https://drafts.csswg.org/css-anchor-position-1/#fallback-apply
auto optionIt = m_positionOptions.find(styleable.element);
if (optionIt == m_positionOptions.end())
return { };
auto& options = optionIt->value;
sortPositionOptionsIfNeeded(options, styleable);
if (!existingStyle) {
options.chosen = true;
return ResolvedStyle { RenderStyle::clonePtr(*options.originalStyle) };
}
auto renderer = dynamicDowncast<RenderBox>(styleable.renderer());
if (!renderer) {
options.chosen = true;
return ResolvedStyle { RenderStyle::clonePtr(*options.originalStyle) };
}
// We can't test for overflow before the box has been positioned.
auto* anchorPositionedState = m_treeResolutionState.anchorPositionedStates.get({ &styleable.element, styleable.pseudoElementIdentifier });
if (anchorPositionedState && anchorPositionedState->stage < AnchorPositionResolutionStage::Positioned)
return ResolvedStyle { RenderStyle::clonePtr(*existingStyle) };
if (!AnchorPositionEvaluator::overflowsInsetModifiedContainingBlock(*renderer)) {
// We don't overflow anymore so this is a good style.
options.chosen = true;
return ResolvedStyle { RenderStyle::clonePtr(*existingStyle) };
}
if (options.chosen) {
// We have already chosen.
return ResolvedStyle { RenderStyle::clonePtr(*existingStyle) };
}
if (options.index >= options.optionStyles.size()) {
// None of the options worked, return back to the original.
options.chosen = true;
return ResolvedStyle { RenderStyle::clonePtr(*options.originalStyle) };
}
auto& optionStyle = options.optionStyles[options.index];
// Next option to try if this doesn't work.
++options.index;
return ResolvedStyle { RenderStyle::clonePtr(*optionStyle) };
}
void TreeResolver::updateForPositionVisibility(RenderStyle& style, const Styleable& styleable)
{
if (!hasResolvedAnchorPosition(styleable))
return;
auto shouldHideAnchorPositioned = [&] {
CheckedPtr anchored = dynamicDowncast<RenderBox>(styleable.renderer());
if (!anchored)
return false;
if (style.positionVisibility().contains(PositionVisibility::AnchorsVisible)) {
// "If the box has a default anchor box but that anchor box is invisible or clipped by intervening boxes, the box’s visibility property computes to force-hidden."
if (AnchorPositionEvaluator::isDefaultAnchorInvisibleOrClippedByInterveningBoxes(*anchored))
return true;
}
// FIXME: Remaining `position-visibility` values.
return false;
};
// FIXME: Implement via "visibility: force-hidden".
if (shouldHideAnchorPositioned())
style.setIsForceHidden();
}
const RenderStyle* TreeResolver::beforeResolutionStyle(const Element& element, std::optional<PseudoElementIdentifier> pseudo)
{
auto resolvePseudoStyle = [&](auto* style) -> const RenderStyle* {
if (!pseudo)
return style;
if (!style)
return nullptr;
return style->getCachedPseudoStyle(*pseudo);
};
auto it = m_savedBeforeResolutionStylesForInterleaving.find(element);
if (it != m_savedBeforeResolutionStylesForInterleaving.end())
return resolvePseudoStyle(it->value.get());
return resolvePseudoStyle(element.renderOrDisplayContentsStyle());
}
void TreeResolver::saveBeforeResolutionStyleForInterleaving(const Element& element)
{
m_savedBeforeResolutionStylesForInterleaving.ensure(element, [&]() -> std::unique_ptr<RenderStyle> {
if (auto* style = element.renderOrDisplayContentsStyle())
return makeUnique<RenderStyle>(RenderStyle::cloneIncludingPseudoElements(*style));
return { };
});
}
bool TreeResolver::hasUnresolvedAnchorPosition(const Styleable& styleable) const
{
auto* anchorPositionedState = m_treeResolutionState.anchorPositionedStates.get({ &styleable.element, styleable.pseudoElementIdentifier });
if (anchorPositionedState && anchorPositionedState->stage < AnchorPositionResolutionStage::Resolved)
return true;
return false;
}
bool TreeResolver::hasResolvedAnchorPosition(const Styleable& styleable) const
{
auto* anchorPositionedState = m_treeResolutionState.anchorPositionedStates.get({ &styleable.element, styleable.pseudoElementIdentifier });
if (anchorPositionedState && anchorPositionedState->stage >= AnchorPositionResolutionStage::Resolved)
return true;
return false;
}
void TreeResolver::collectChangedAnchorNames(const RenderStyle& newStyle, const RenderStyle* currentStyle)
{
// A changed anchor name is either a name being added, a name being removed, or a name whose interpretation changes.
// This may change which elements get anchored to it.
if (!currentStyle || currentStyle->anchorNames() != newStyle.anchorNames()) {
// This could check which individual names differ but usually there is just one.
auto addChanged = [&](auto& style) {
for (auto& name : style.anchorNames())
m_changedAnchorNames.add(name.name);
};
if (currentStyle)
addChanged(*currentStyle);
addChanged(newStyle);
}
// Only anchor-names in self and descendants are affected by anchor-scope so we need to check only if there is an existing style.
if (currentStyle && currentStyle->anchorScope() != newStyle.anchorScope()) {
auto addChanged = [&](auto& style) {
switch (style.anchorScope().type) {
case NameScope::Type::None:
break;
case NameScope::Type::All:
// This affects desdendants too so lets just say all names are invalid.
m_allAnchorNamesInvalid = true;
break;
case NameScope::Type::Ident:
// A scope change changes interpretation of these names.
for (auto& name : style.anchorScope().names)
m_changedAnchorNames.add(name);
break;
}
};
addChanged(*currentStyle);
addChanged(newStyle);
}
}
static Vector<Function<void ()>>& postResolutionCallbackQueue()
{
static NeverDestroyed<Vector<Function<void ()>>> vector;
return vector;
}
static Vector<RefPtr<LocalFrame>>& memoryCacheClientCallsResumeQueue()
{
static NeverDestroyed<Vector<RefPtr<LocalFrame>>> vector;
return vector;
}
void deprecatedQueuePostResolutionCallback(Function<void()>&& callback)
{
postResolutionCallbackQueue().append(WTFMove(callback));
}
static void suspendMemoryCacheClientCalls(Document& document)
{
Page* page = document.page();
if (!page || !page->areMemoryCacheClientCallsEnabled())
return;
page->setMemoryCacheClientCallsEnabled(false);
if (RefPtr localMainFrame = page->localMainFrame())
memoryCacheClientCallsResumeQueue().append(localMainFrame);
}
static unsigned resolutionNestingDepth;
PostResolutionCallbackDisabler::PostResolutionCallbackDisabler(Document& document, DrainCallbacks drainCallbacks)
: m_drainCallbacks(drainCallbacks)
{
++resolutionNestingDepth;
if (resolutionNestingDepth == 1)
platformStrategies()->loaderStrategy()->suspendPendingRequests();
// FIXME: It's strange to build this into the disabler.
suspendMemoryCacheClientCalls(document);
}
PostResolutionCallbackDisabler::~PostResolutionCallbackDisabler()
{
if (resolutionNestingDepth == 1) {
if (m_drainCallbacks == DrainCallbacks::Yes) {
// Get size each time through the loop because a callback can add more callbacks to the end of the queue.
auto& queue = postResolutionCallbackQueue();
for (size_t i = 0; i < queue.size(); ++i)
queue[i]();
queue.clear();
}
auto& queue = memoryCacheClientCallsResumeQueue();
for (size_t i = 0; i < queue.size(); ++i) {
if (RefPtr page = queue[i]->page())
page->setMemoryCacheClientCallsEnabled(true);
}
queue.clear();
platformStrategies()->loaderStrategy()->resumePendingRequests();
}
--resolutionNestingDepth;
}
bool postResolutionCallbacksAreSuspended()
{
return resolutionNestingDepth;
}
}
}