Source code

Revision control

Copy as Markdown

Other Tools

/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set ts=8 sts=2 et sw=2 tw=80: */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#include "mozilla/RestyleManager.h"
#include "mozilla/AnimationUtils.h"
#include "mozilla/Assertions.h"
#include "mozilla/ComputedStyle.h"
#include "mozilla/ComputedStyleInlines.h"
#include "mozilla/DocumentStyleRootIterator.h"
#include "mozilla/EffectSet.h"
#include "mozilla/GeckoBindings.h"
#include "mozilla/LayerAnimationInfo.h"
#include "mozilla/layers/AnimationInfo.h"
#include "mozilla/layout/ScrollAnchorContainer.h"
#include "mozilla/PresShell.h"
#include "mozilla/PresShellInlines.h"
#include "mozilla/ProfilerLabels.h"
#include "mozilla/ScrollContainerFrame.h"
#include "mozilla/ServoBindings.h"
#include "mozilla/ServoStyleSetInlines.h"
#include "mozilla/StaticPrefs_layout.h"
#include "mozilla/SVGIntegrationUtils.h"
#include "mozilla/SVGObserverUtils.h"
#include "mozilla/SVGTextFrame.h"
#include "mozilla/SVGUtils.h"
#include "mozilla/Unused.h"
#include "mozilla/ViewportFrame.h"
#include "mozilla/IntegerRange.h"
#include "mozilla/dom/ChildIterator.h"
#include "mozilla/dom/DocumentInlines.h"
#include "mozilla/dom/ElementInlines.h"
#include "mozilla/dom/HTMLBodyElement.h"
#include "mozilla/dom/HTMLInputElement.h"
#include "ScrollSnap.h"
#include "nsAnimationManager.h"
#include "nsBlockFrame.h"
#include "nsContentUtils.h"
#include "nsCSSFrameConstructor.h"
#include "nsCSSRendering.h"
#include "nsDocShell.h"
#include "nsIFrame.h"
#include "nsIFrameInlines.h"
#include "nsImageFrame.h"
#include "nsPlaceholderFrame.h"
#include "nsPrintfCString.h"
#include "nsRefreshDriver.h"
#include "nsStyleChangeList.h"
#include "nsStyleUtil.h"
#include "nsTransitionManager.h"
#include "StickyScrollContainer.h"
#include "ActiveLayerTracker.h"
#ifdef ACCESSIBILITY
# include "nsAccessibilityService.h"
#endif
using mozilla::layers::AnimationInfo;
using mozilla::layout::ScrollAnchorContainer;
using namespace mozilla::dom;
using namespace mozilla::layers;
namespace mozilla {
RestyleManager::RestyleManager(nsPresContext* aPresContext)
: mPresContext(aPresContext),
mRestyleGeneration(1),
mUndisplayedRestyleGeneration(1),
mInStyleRefresh(false),
mAnimationGeneration(0) {
MOZ_ASSERT(mPresContext);
}
void RestyleManager::ContentInserted(nsIContent* aChild) {
MOZ_ASSERT(aChild->GetParentNode());
if (aChild->IsElement()) {
StyleSet()->MaybeInvalidateForElementInsertion(*aChild->AsElement());
}
RestyleForInsertOrChange(aChild);
}
void RestyleManager::ContentAppended(nsIContent* aFirstNewContent) {
auto* container = aFirstNewContent->GetParentNode();
MOZ_ASSERT(container);
#ifdef DEBUG
{
for (nsIContent* cur = aFirstNewContent; cur; cur = cur->GetNextSibling()) {
NS_ASSERTION(!cur->IsRootOfNativeAnonymousSubtree(),
"anonymous nodes should not be in child lists");
}
}
#endif
StyleSet()->MaybeInvalidateForElementAppend(*aFirstNewContent);
const auto selectorFlags = container->GetSelectorFlags() &
NodeSelectorFlags::AllSimpleRestyleFlagsForAppend;
if (!selectorFlags) {
return;
}
// The container cannot be a document.
MOZ_ASSERT(container->IsElement() || container->IsShadowRoot());
if (selectorFlags & NodeSelectorFlags::HasEmptySelector) {
// see whether we need to restyle the container
bool wasEmpty = true; // :empty or :-moz-only-whitespace
for (nsIContent* cur = container->GetFirstChild(); cur != aFirstNewContent;
cur = cur->GetNextSibling()) {
// We don't know whether we're testing :empty or :-moz-only-whitespace,
// so be conservative and assume :-moz-only-whitespace (i.e., make
// IsSignificantChild less likely to be true, and thus make us more
// likely to restyle).
if (nsStyleUtil::IsSignificantChild(cur, false)) {
wasEmpty = false;
break;
}
}
if (wasEmpty && container->IsElement()) {
RestyleForEmptyChange(container->AsElement());
return;
}
}
if (selectorFlags & NodeSelectorFlags::HasSlowSelector) {
if (container->IsElement()) {
auto* containerElement = container->AsElement();
PostRestyleEvent(containerElement, RestyleHint::RestyleSubtree(),
nsChangeHint(0));
if (selectorFlags & NodeSelectorFlags::HasSlowSelectorNthAll) {
StyleSet()->MaybeInvalidateRelativeSelectorForNthDependencyFromSibling(
containerElement->GetFirstElementChild(),
/* aForceRestyleSiblings = */ false);
}
} else {
RestylePreviousSiblings(aFirstNewContent);
RestyleSiblingsStartingWith(aFirstNewContent);
}
// Restyling the container is the most we can do here, so we're done.
return;
}
if (selectorFlags & NodeSelectorFlags::HasEdgeChildSelector) {
// restyle the last element child before this node
for (nsIContent* cur = aFirstNewContent->GetPreviousSibling(); cur;
cur = cur->GetPreviousSibling()) {
if (cur->IsElement()) {
auto* element = cur->AsElement();
PostRestyleEvent(element, RestyleHint::RestyleSubtree(),
nsChangeHint(0));
StyleSet()->MaybeInvalidateRelativeSelectorForNthEdgeDependency(
*element);
break;
}
}
}
}
void RestyleManager::RestylePreviousSiblings(nsIContent* aStartingSibling) {
for (nsIContent* sibling = aStartingSibling; sibling;
sibling = sibling->GetPreviousSibling()) {
if (auto* element = Element::FromNode(sibling)) {
PostRestyleEvent(element, RestyleHint::RestyleSubtree(), nsChangeHint(0));
}
}
}
void RestyleManager::RestyleSiblingsStartingWith(nsIContent* aStartingSibling) {
for (nsIContent* sibling = aStartingSibling; sibling;
sibling = sibling->GetNextSibling()) {
if (auto* element = Element::FromNode(sibling)) {
PostRestyleEvent(element, RestyleHint::RestyleSubtree(), nsChangeHint(0));
}
}
}
void RestyleManager::RestyleForEmptyChange(Element* aContainer) {
PostRestyleEvent(aContainer, RestyleHint::RestyleSubtree(), nsChangeHint(0));
StyleSet()->MaybeInvalidateRelativeSelectorForEmptyDependency(*aContainer);
// In some cases (:empty + E, :empty ~ E), a change in the content of
// an element requires restyling its parent's siblings.
nsIContent* grandparent = aContainer->GetParent();
if (!grandparent || !(grandparent->GetSelectorFlags() &
NodeSelectorFlags::HasSlowSelectorLaterSiblings)) {
return;
}
RestyleSiblingsStartingWith(aContainer->GetNextSibling());
}
void RestyleManager::MaybeRestyleForEdgeChildChange(nsINode* aContainer,
nsIContent* aChangedChild) {
MOZ_ASSERT(aContainer->GetSelectorFlags() &
NodeSelectorFlags::HasEdgeChildSelector);
MOZ_ASSERT(aChangedChild->GetParent() == aContainer);
// restyle the previously-first element child if it is after this node
bool passedChild = false;
for (nsIContent* content = aContainer->GetFirstChild(); content;
content = content->GetNextSibling()) {
if (content == aChangedChild) {
passedChild = true;
continue;
}
if (content->IsElement()) {
if (passedChild) {
auto* element = content->AsElement();
PostRestyleEvent(element, RestyleHint::RestyleSubtree(),
nsChangeHint(0));
StyleSet()->MaybeInvalidateRelativeSelectorForNthEdgeDependency(
*element);
}
break;
}
}
// restyle the previously-last element child if it is before this node
passedChild = false;
for (nsIContent* content = aContainer->GetLastChild(); content;
content = content->GetPreviousSibling()) {
if (content == aChangedChild) {
passedChild = true;
continue;
}
if (content->IsElement()) {
if (passedChild) {
auto* element = content->AsElement();
PostRestyleEvent(element, RestyleHint::RestyleSubtree(),
nsChangeHint(0));
StyleSet()->MaybeInvalidateRelativeSelectorForNthEdgeDependency(
*element);
}
break;
}
}
}
template <typename CharT>
bool WhitespaceOnly(const CharT* aBuffer, size_t aUpTo) {
for (auto index : IntegerRange(aUpTo)) {
if (!dom::IsSpaceCharacter(aBuffer[index])) {
return false;
}
}
return true;
}
template <typename CharT>
bool WhitespaceOnlyChangedOnAppend(const CharT* aBuffer, size_t aOldLength,
size_t aNewLength) {
MOZ_ASSERT(aOldLength <= aNewLength);
if (!WhitespaceOnly(aBuffer, aOldLength)) {
// The old text was already not whitespace-only.
return false;
}
return !WhitespaceOnly(aBuffer + aOldLength, aNewLength - aOldLength);
}
static bool HasAnySignificantSibling(Element* aContainer, nsIContent* aChild) {
MOZ_ASSERT(aChild->GetParent() == aContainer);
for (nsIContent* child = aContainer->GetFirstChild(); child;
child = child->GetNextSibling()) {
if (child == aChild) {
continue;
}
// We don't know whether we're testing :empty or :-moz-only-whitespace,
// so be conservative and assume :-moz-only-whitespace (i.e., make
// IsSignificantChild less likely to be true, and thus make us more
// likely to restyle).
if (nsStyleUtil::IsSignificantChild(child, false)) {
return true;
}
}
return false;
}
void RestyleManager::CharacterDataChanged(
nsIContent* aContent, const CharacterDataChangeInfo& aInfo) {
nsINode* parent = aContent->GetParentNode();
MOZ_ASSERT(parent, "How were we notified of a stray node?");
const auto slowSelectorFlags =
parent->GetSelectorFlags() & NodeSelectorFlags::AllSimpleRestyleFlags;
if (!(slowSelectorFlags & (NodeSelectorFlags::HasEmptySelector |
NodeSelectorFlags::HasEdgeChildSelector))) {
// Nothing to do, no other slow selector can change as a result of this.
return;
}
if (!aContent->IsText()) {
// Doesn't matter to styling (could be a processing instruction or a
// comment), it can't change whether any selectors match or don't.
return;
}
if (MOZ_UNLIKELY(!parent->IsElement())) {
MOZ_ASSERT(parent->IsShadowRoot());
return;
}
if (MOZ_UNLIKELY(aContent->IsRootOfNativeAnonymousSubtree())) {
// This is an anonymous node and thus isn't in child lists, so isn't taken
// into account for selector matching the relevant selectors here.
return;
}
// Handle appends specially since they're common and we can know both the old
// and the new text exactly.
//
// TODO(emilio): This could be made much more general if :-moz-only-whitespace
// / :-moz-first-node and :-moz-last-node didn't exist. In that case we only
// need to know whether we went from empty to non-empty, and that's trivial to
// know, with CharacterDataChangeInfo...
if (!aInfo.mAppend) {
// FIXME(emilio): This restyles unnecessarily if the text node is the only
// child of the parent element. Fortunately, it's uncommon to have such
// nodes and this not being an append.
//
// See the testcase in bug 1427625 for a test-case that triggers this.
RestyleForInsertOrChange(aContent);
return;
}
const nsTextFragment* text = &aContent->AsText()->TextFragment();
const size_t oldLength = aInfo.mChangeStart;
const size_t newLength = text->GetLength();
const bool emptyChanged = !oldLength && newLength;
const bool whitespaceOnlyChanged =
text->Is2b()
? WhitespaceOnlyChangedOnAppend(text->Get2b(), oldLength, newLength)
: WhitespaceOnlyChangedOnAppend(text->Get1b(), oldLength, newLength);
if (!emptyChanged && !whitespaceOnlyChanged) {
return;
}
if (slowSelectorFlags & NodeSelectorFlags::HasEmptySelector) {
if (!HasAnySignificantSibling(parent->AsElement(), aContent)) {
// We used to be empty, restyle the parent.
RestyleForEmptyChange(parent->AsElement());
return;
}
}
if (slowSelectorFlags & NodeSelectorFlags::HasEdgeChildSelector) {
MaybeRestyleForEdgeChildChange(parent, aContent);
}
}
// Restyling for a ContentInserted or CharacterDataChanged notification.
// This could be used for ContentRemoved as well if we got the
// notification before the removal happened (and sometimes
// CharacterDataChanged is more like a removal than an addition).
// The comments are written and variables are named in terms of it being
// a ContentInserted notification.
void RestyleManager::RestyleForInsertOrChange(nsIContent* aChild) {
nsINode* container = aChild->GetParentNode();
MOZ_ASSERT(container);
const auto selectorFlags =
container->GetSelectorFlags() & NodeSelectorFlags::AllSimpleRestyleFlags;
if (!selectorFlags) {
return;
}
NS_ASSERTION(!aChild->IsRootOfNativeAnonymousSubtree(),
"anonymous nodes should not be in child lists");
// The container cannot be a document.
MOZ_ASSERT(container->IsElement() || container->IsShadowRoot());
if (selectorFlags & NodeSelectorFlags::HasEmptySelector &&
container->IsElement()) {
// See whether we need to restyle the container due to :empty /
// :-moz-only-whitespace.
const bool wasEmpty =
!HasAnySignificantSibling(container->AsElement(), aChild);
if (wasEmpty) {
// FIXME(emilio): When coming from CharacterDataChanged this can restyle
// unnecessarily. Also can restyle unnecessarily if aChild is not
// significant anyway, though that's more unlikely.
RestyleForEmptyChange(container->AsElement());
return;
}
}
if (selectorFlags & NodeSelectorFlags::HasSlowSelector) {
if (container->IsElement()) {
auto* containerElement = container->AsElement();
PostRestyleEvent(containerElement, RestyleHint::RestyleSubtree(),
nsChangeHint(0));
if (selectorFlags & NodeSelectorFlags::HasSlowSelectorNthAll) {
StyleSet()->MaybeInvalidateRelativeSelectorForNthDependencyFromSibling(
containerElement->GetFirstElementChild(),
/* aForceRestyleSiblings = */ false);
}
} else {
RestylePreviousSiblings(aChild);
RestyleSiblingsStartingWith(aChild);
}
// Restyling the container is the most we can do here, so we're done.
return;
}
if (selectorFlags & NodeSelectorFlags::HasSlowSelectorLaterSiblings) {
// Restyle all later siblings.
if (selectorFlags & NodeSelectorFlags::HasSlowSelectorNthAll) {
StyleSet()->MaybeInvalidateRelativeSelectorForNthDependencyFromSibling(
aChild->GetNextElementSibling(), /* aForceRestyleSiblings = */ true);
} else {
RestyleSiblingsStartingWith(aChild->GetNextSibling());
}
}
if (selectorFlags & NodeSelectorFlags::HasEdgeChildSelector) {
MaybeRestyleForEdgeChildChange(container, aChild);
}
}
void RestyleManager::ContentRemoved(nsIContent* aOldChild,
nsIContent* aFollowingSibling) {
auto* container = aOldChild->GetParentNode();
MOZ_ASSERT(container);
// Computed style data isn't useful for detached nodes, and we'll need to
// recompute it anyway if we ever insert the nodes back into a document.
if (auto* element = Element::FromNode(aOldChild)) {
RestyleManager::ClearServoDataFromSubtree(element);
// If this element is undisplayed or may have undisplayed descendants, we
// need to invalidate the cache, since there's the unlikely event of those
// elements getting destroyed and their addresses reused in a way that we
// look up the cache with their address for a different element before it's
// invalidated.
IncrementUndisplayedRestyleGeneration();
}
if (aOldChild->IsElement()) {
StyleSet()->MaybeInvalidateForElementRemove(*aOldChild->AsElement(),
aFollowingSibling);
}
const auto selectorFlags =
container->GetSelectorFlags() & NodeSelectorFlags::AllSimpleRestyleFlags;
if (!selectorFlags) {
return;
}
if (aOldChild->IsRootOfNativeAnonymousSubtree()) {
// This should be an assert, but this is called incorrectly in
// HTMLEditor::DeleteRefToAnonymousNode and the assertions were clogging
// up the logs. Make it an assert again when that's fixed.
MOZ_ASSERT(aOldChild->GetProperty(nsGkAtoms::restylableAnonymousNode),
"anonymous nodes should not be in child lists (bug 439258)");
}
// The container cannot be a document.
MOZ_ASSERT(container->IsElement() || container->IsShadowRoot());
if (selectorFlags & NodeSelectorFlags::HasEmptySelector &&
container->IsElement()) {
// see whether we need to restyle the container
bool isEmpty = true; // :empty or :-moz-only-whitespace
for (nsIContent* child = container->GetFirstChild(); child;
child = child->GetNextSibling()) {
// We don't know whether we're testing :empty or :-moz-only-whitespace,
// so be conservative and assume :-moz-only-whitespace (i.e., make
// IsSignificantChild less likely to be true, and thus make us more
// likely to restyle).
if (nsStyleUtil::IsSignificantChild(child, false)) {
isEmpty = false;
break;
}
}
if (isEmpty && container->IsElement()) {
RestyleForEmptyChange(container->AsElement());
return;
}
}
if (selectorFlags & NodeSelectorFlags::HasSlowSelector) {
if (container->IsElement()) {
auto* containerElement = container->AsElement();
PostRestyleEvent(containerElement, RestyleHint::RestyleSubtree(),
nsChangeHint(0));
if (selectorFlags & NodeSelectorFlags::HasSlowSelectorNthAll) {
StyleSet()->MaybeInvalidateRelativeSelectorForNthDependencyFromSibling(
containerElement->GetFirstElementChild(),
/* aForceRestyleSiblings = */ false);
}
} else {
RestylePreviousSiblings(aOldChild);
RestyleSiblingsStartingWith(aOldChild);
}
// Restyling the container is the most we can do here, so we're done.
return;
}
if (selectorFlags & NodeSelectorFlags::HasSlowSelectorLaterSiblings) {
// Restyle all later siblings.
if (selectorFlags & NodeSelectorFlags::HasSlowSelectorNthAll) {
Element* nextSibling =
aFollowingSibling ? aFollowingSibling->IsElement()
? aFollowingSibling->AsElement()
: aFollowingSibling->GetNextElementSibling()
: nullptr;
StyleSet()->MaybeInvalidateRelativeSelectorForNthDependencyFromSibling(
nextSibling, /* aForceRestyleSiblings = */ true);
} else {
RestyleSiblingsStartingWith(aFollowingSibling);
}
}
if (selectorFlags & NodeSelectorFlags::HasEdgeChildSelector) {
// restyle the now-first element child if it was after aOldChild
bool reachedFollowingSibling = false;
for (nsIContent* content = container->GetFirstChild(); content;
content = content->GetNextSibling()) {
if (content == aFollowingSibling) {
reachedFollowingSibling = true;
// do NOT continue here; we might want to restyle this node
}
if (content->IsElement()) {
if (reachedFollowingSibling) {
auto* element = content->AsElement();
PostRestyleEvent(element, RestyleHint::RestyleSubtree(),
nsChangeHint(0));
StyleSet()->MaybeInvalidateRelativeSelectorForNthEdgeDependency(
*element);
}
break;
}
}
// restyle the now-last element child if it was before aOldChild
reachedFollowingSibling = (aFollowingSibling == nullptr);
for (nsIContent* content = container->GetLastChild(); content;
content = content->GetPreviousSibling()) {
if (content->IsElement()) {
if (reachedFollowingSibling) {
auto* element = content->AsElement();
PostRestyleEvent(element, RestyleHint::RestyleSubtree(),
nsChangeHint(0));
StyleSet()->MaybeInvalidateRelativeSelectorForNthEdgeDependency(
*element);
}
break;
}
if (content == aFollowingSibling) {
reachedFollowingSibling = true;
}
}
}
}
static bool StateChangeMayAffectFrame(const Element& aElement,
const nsIFrame& aFrame,
ElementState aStates) {
const bool brokenChanged = aStates.HasState(ElementState::BROKEN);
if (!brokenChanged) {
return false;
}
if (aFrame.IsGeneratedContentFrame()) {
// If it's other generated content, ignore state changes on it.
return aElement.IsHTMLElement(nsGkAtoms::mozgeneratedcontentimage);
}
if (aElement.IsAnyOfHTMLElements(nsGkAtoms::object, nsGkAtoms::embed)) {
// Broken affects object fallback behavior.
return true;
}
const bool mightChange = [&] {
if (aElement.IsHTMLElement(nsGkAtoms::img)) {
return true;
}
const auto* input = HTMLInputElement::FromNode(aElement);
return input && input->ControlType() == FormControlType::InputImage;
}();
if (!mightChange) {
return false;
}
const bool needsImageFrame =
nsImageFrame::ImageFrameTypeFor(aElement, *aFrame.Style()) !=
nsImageFrame::ImageFrameType::None;
return needsImageFrame != aFrame.IsImageFrameOrSubclass();
}
static bool RepaintForAppearance(nsIFrame& aFrame, const Element& aElement,
ElementState aStateMask) {
if (aStateMask.HasAtLeastOneOfStates(ElementState::HOVER |
ElementState::ACTIVE) &&
aElement.IsAnyOfXULElements(nsGkAtoms::checkbox, nsGkAtoms::radio)) {
// The checkbox inside these elements inherit hover state and so on, see
// nsNativeTheme::GetContentState.
// FIXME(emilio): Would be nice to not have these hard-coded.
return true;
}
auto appearance = aFrame.StyleDisplay()->EffectiveAppearance();
if (appearance == StyleAppearance::None) {
return false;
}
nsPresContext* pc = aFrame.PresContext();
nsITheme* theme = pc->Theme();
if (!theme->ThemeSupportsWidget(pc, &aFrame, appearance)) {
return false;
}
bool repaint = false;
theme->WidgetStateChanged(&aFrame, appearance, nullptr, &repaint, nullptr);
return repaint;
}
/**
* Calculates the change hint and the restyle hint for a given content state
* change.
*/
static nsChangeHint ChangeForContentStateChange(const Element& aElement,
ElementState aStateMask) {
auto changeHint = nsChangeHint(0);
// Any change to a content state that affects which frames we construct
// must lead to a frame reconstruct here if we already have a frame.
// Note that we never decide through non-CSS means to not create frames
// based on content states, so if we already don't have a frame we don't
// need to force a reframe -- if it's needed, the HasStateDependentStyle
// call will handle things.
if (nsIFrame* primaryFrame = aElement.GetPrimaryFrame()) {
if (StateChangeMayAffectFrame(aElement, *primaryFrame, aStateMask)) {
return nsChangeHint_ReconstructFrame;
}
if (RepaintForAppearance(*primaryFrame, aElement, aStateMask)) {
changeHint |= nsChangeHint_RepaintFrame;
}
primaryFrame->ElementStateChanged(aStateMask);
}
if (aStateMask.HasState(ElementState::VISITED)) {
// Exposing information to the page about whether the link is
// visited or not isn't really something we can worry about here.
// FIXME: We could probably do this a bit better.
changeHint |= nsChangeHint_RepaintFrame;
}
// This changes the applicable text-transform in the editor root.
if (aStateMask.HasState(ElementState::REVEALED)) {
// This is the same change hint as tweaking text-transform.
changeHint |= NS_STYLE_HINT_REFLOW;
}
return changeHint;
}
#ifdef DEBUG
/* static */
nsCString RestyleManager::ChangeHintToString(nsChangeHint aHint) {
nsCString result;
bool any = false;
const char* names[] = {"RepaintFrame",
"NeedReflow",
"ClearAncestorIntrinsics",
"ClearDescendantIntrinsics",
"NeedDirtyReflow",
"UpdateCursor",
"UpdateEffects",
"UpdateOpacityLayer",
"UpdateTransformLayer",
"ReconstructFrame",
"UpdateOverflow",
"UpdateSubtreeOverflow",
"UpdatePostTransformOverflow",
"UpdateParentOverflow",
"ChildrenOnlyTransform",
"RecomputePosition",
"UpdateContainingBlock",
"BorderStyleNoneChange",
"SchedulePaint",
"NeutralChange",
"InvalidateRenderingObservers",
"ReflowChangesSizeOrPosition",
"UpdateComputedBSize",
"UpdateUsesOpacity",
"UpdateBackgroundPosition",
"AddOrRemoveTransform",
"ScrollbarChange",
"UpdateTableCellSpans",
"VisibilityChange"};
static_assert(nsChangeHint_AllHints ==
static_cast<uint32_t>((1ull << ArrayLength(names)) - 1),
"Name list doesn't match change hints.");
uint32_t hint =
aHint & static_cast<uint32_t>((1ull << ArrayLength(names)) - 1);
uint32_t rest =
aHint & ~static_cast<uint32_t>((1ull << ArrayLength(names)) - 1);
if ((hint & NS_STYLE_HINT_REFLOW) == NS_STYLE_HINT_REFLOW) {
result.AppendLiteral("NS_STYLE_HINT_REFLOW");
hint = hint & ~NS_STYLE_HINT_REFLOW;
any = true;
} else if ((hint & nsChangeHint_AllReflowHints) ==
nsChangeHint_AllReflowHints) {
result.AppendLiteral("nsChangeHint_AllReflowHints");
hint = hint & ~nsChangeHint_AllReflowHints;
any = true;
} else if ((hint & NS_STYLE_HINT_VISUAL) == NS_STYLE_HINT_VISUAL) {
result.AppendLiteral("NS_STYLE_HINT_VISUAL");
hint = hint & ~NS_STYLE_HINT_VISUAL;
any = true;
}
for (uint32_t i = 0; i < ArrayLength(names); i++) {
if (hint & (1u << i)) {
if (any) {
result.AppendLiteral(" | ");
}
result.AppendPrintf("nsChangeHint_%s", names[i]);
any = true;
}
}
if (rest) {
if (any) {
result.AppendLiteral(" | ");
}
result.AppendPrintf("0x%0x", rest);
} else {
if (!any) {
result.AppendLiteral("nsChangeHint(0)");
}
}
return result;
}
#endif
/**
* Frame construction helpers follow.
*/
#ifdef DEBUG
static bool gInApplyRenderingChangeToTree = false;
#endif
/**
* Sync views on the frame and all of it's descendants (following placeholders).
* The change hint should be some combination of nsChangeHint_RepaintFrame,
* nsChangeHint_UpdateOpacityLayer and nsChangeHint_SchedulePaint, nothing else.
*/
static void SyncViewsAndInvalidateDescendants(nsIFrame*, nsChangeHint);
static void StyleChangeReflow(nsIFrame* aFrame, nsChangeHint aHint);
/**
* This helper function is used to find the correct SVG frame to target when we
* encounter nsChangeHint_ChildrenOnlyTransform; needed since sometimes we end
* up handling that hint while processing hints for one of the SVG frame's
* ancestor frames.
*
* The reason that we sometimes end up trying to process the hint for an
* ancestor of the SVG frame that the hint is intended for is due to the way we
* process restyle events. ApplyRenderingChangeToTree adjusts the frame from
* the restyled element's principle frame to one of its ancestor frames based
* on what nsCSSRendering::FindBackground returns, since the background style
* may have been propagated up to an ancestor frame. Processing hints using an
* ancestor frame is fine in general, but nsChangeHint_ChildrenOnlyTransform is
* a special case since it is intended to update a specific frame.
*/
static nsIFrame* GetFrameForChildrenOnlyTransformHint(nsIFrame* aFrame) {
if (aFrame->IsViewportFrame()) {
// This happens if the root-<svg> is fixed positioned, in which case we
// can't use aFrame->GetContent() to find the primary frame, since
// GetContent() returns nullptr for ViewportFrame.
aFrame = aFrame->PrincipalChildList().FirstChild();
}
// For a ScrollContainerFrame, this will get the SVG frame that has the
// children-only transforms:
aFrame = aFrame->GetContent()->GetPrimaryFrame();
if (aFrame->IsSVGOuterSVGFrame()) {
aFrame = aFrame->PrincipalChildList().FirstChild();
MOZ_ASSERT(aFrame->IsSVGOuterSVGAnonChildFrame(),
"Where is the SVGOuterSVGFrame's anon child??");
}
MOZ_ASSERT(aFrame->IsSVGContainerFrame(),
"Children-only transforms only expected on SVG frames");
return aFrame;
}
// This function tries to optimize a position style change by either
// moving aFrame or ignoring the style change when it's safe to do so.
// It returns true when that succeeds, otherwise it posts a reflow request
// and returns false.
static bool RecomputePosition(nsIFrame* aFrame) {
// It's pointless to move around frames that have never been reflowed or
// are dirty (i.e. they will be reflowed), or aren't affected by position
// styles.
if (aFrame->HasAnyStateBits(NS_FRAME_FIRST_REFLOW | NS_FRAME_IS_DIRTY |
NS_FRAME_SVG_LAYOUT)) {
return true;
}
// Don't process position changes on table frames, since we already handle
// the dynamic position change on the table wrapper frame, and the
// reflow-based fallback code path also ignores positions on inner table
// frames.
if (aFrame->IsTableFrame()) {
return true;
}
const nsStyleDisplay* display = aFrame->StyleDisplay();
// Changes to the offsets of a non-positioned element can safely be ignored.
if (display->mPosition == StylePositionProperty::Static) {
return true;
}
// Don't process position changes on frames which have views or the ones which
// have a view somewhere in their descendants, because the corresponding view
// needs to be repositioned properly as well.
if (aFrame->HasView() ||
aFrame->HasAnyStateBits(NS_FRAME_HAS_CHILD_WITH_VIEW)) {
return false;
}
if (aFrame->HasAnyStateBits(NS_FRAME_OUT_OF_FLOW)) {
// If the frame has an intrinsic block-size, we resolve its 'auto' margins
// after doing layout, since we need to know the frame's block size. See
// nsAbsoluteContainingBlock::ResolveAutoMarginsAfterLayout().
//
// Since the size of the frame doesn't change, we could modify the below
// computation to compute the margin correctly without doing a full reflow,
// however we decided to try doing a full reflow for now.
if (aFrame->HasIntrinsicKeywordForBSize()) {
WritingMode wm = aFrame->GetWritingMode();
const auto* styleMargin = aFrame->StyleMargin();
if (styleMargin->HasBlockAxisAuto(wm)) {
return false;
}
}
// Flexbox and Grid layout supports CSS Align and the optimizations below
// don't support that yet.
nsIFrame* ph = aFrame->GetPlaceholderFrame();
if (ph && ph->HasAnyStateBits(PLACEHOLDER_STATICPOS_NEEDS_CSSALIGN)) {
return false;
}
}
// If we need to reposition any descendant that depends on our static
// position, then we also can't take the optimized path.
//
// TODO(emilio): It may be worth trying to find them and try to call
// RecomputePosition on them too instead of disabling the optimization...
if (aFrame->DescendantMayDependOnItsStaticPosition()) {
return false;
}
aFrame->SchedulePaint();
auto postPendingScrollAnchorOrResnap = [](nsIFrame* frame) {
if (frame->IsInScrollAnchorChain()) {
ScrollAnchorContainer* container = ScrollAnchorContainer::FindFor(frame);
frame->PresShell()->PostPendingScrollAnchorAdjustment(container);
}
// We need to trigger re-snapping to this content if we snapped to the
// content on the last scroll operation.
ScrollSnapUtils::PostPendingResnapIfNeededFor(frame);
};
// For relative positioning, we can simply update the frame rect
if (display->IsRelativelyOrStickyPositionedStyle()) {
if (aFrame->IsGridItem()) {
// A grid item's CB is its grid area, not the parent frame content area
// as is assumed below.
return false;
}
// Move the frame
if (display->mPosition == StylePositionProperty::Sticky) {
// Update sticky positioning for an entire element at once, starting with
// the first continuation or ib-split sibling.
// It's rare that the frame we already have isn't already the first
// continuation or ib-split sibling, but it can happen when styles differ
// across continuations such as ::first-line or ::first-letter, and in
// those cases we will generally (but maybe not always) do the work twice.
nsIFrame* firstContinuation =
nsLayoutUtils::FirstContinuationOrIBSplitSibling(aFrame);
StickyScrollContainer::ComputeStickyOffsets(firstContinuation);
StickyScrollContainer* ssc =
StickyScrollContainer::GetStickyScrollContainerForFrame(
firstContinuation);
if (ssc) {
ssc->PositionContinuations(firstContinuation);
}
} else {
MOZ_ASSERT(display->IsRelativelyPositionedStyle(),
"Unexpected type of positioning");
for (nsIFrame* cont = aFrame; cont;
cont = nsLayoutUtils::GetNextContinuationOrIBSplitSibling(cont)) {
nsIFrame* cb = cont->GetContainingBlock();
WritingMode wm = cb->GetWritingMode();
const LogicalSize cbSize = cb->ContentSize();
const LogicalMargin newLogicalOffsets =
ReflowInput::ComputeRelativeOffsets(wm, cont, cbSize);
const nsMargin newOffsets = newLogicalOffsets.GetPhysicalMargin(wm);
// ReflowInput::ApplyRelativePositioning would work here, but
// since we've already checked mPosition and aren't changing the frame's
// normal position, go ahead and add the offsets directly.
// First, we need to ensure that the normal position is stored though.
bool hasProperty;
nsPoint normalPosition = cont->GetNormalPosition(&hasProperty);
if (!hasProperty) {
cont->AddProperty(nsIFrame::NormalPositionProperty(), normalPosition);
}
cont->SetPosition(normalPosition +
nsPoint(newOffsets.left, newOffsets.top));
}
}
postPendingScrollAnchorOrResnap(aFrame);
return true;
}
// For the absolute positioning case, set up a fake HTML reflow input for
// the frame, and then get the offsets and size from it. If the frame's size
// doesn't need to change, we can simply update the frame position. Otherwise
// we fall back to a reflow.
UniquePtr<gfxContext> rc =
aFrame->PresShell()->CreateReferenceRenderingContext();
// Construct a bogus parent reflow input so that there's a usable reflow input
// for the containing block.
nsIFrame* parentFrame = aFrame->GetParent();
WritingMode parentWM = parentFrame->GetWritingMode();
WritingMode frameWM = aFrame->GetWritingMode();
LogicalSize parentSize = parentFrame->GetLogicalSize();
nsFrameState savedState = parentFrame->GetStateBits();
ReflowInput parentReflowInput(aFrame->PresContext(), parentFrame, rc.get(),
parentSize);
parentFrame->RemoveStateBits(~nsFrameState(0));
parentFrame->AddStateBits(savedState);
// The bogus parent state here was created with no parent state of its own,
// and therefore it won't have an mCBReflowInput set up.
// But we may need one (for InitCBReflowInput in a child state), so let's
// try to create one here for the cases where it will be needed.
Maybe<ReflowInput> cbReflowInput;
nsIFrame* cbFrame = parentFrame->GetContainingBlock();
if (cbFrame && (aFrame->GetContainingBlock() != parentFrame ||
parentFrame->IsTableFrame())) {
const auto cbWM = cbFrame->GetWritingMode();
LogicalSize cbSize = cbFrame->GetLogicalSize();
cbReflowInput.emplace(cbFrame->PresContext(), cbFrame, rc.get(), cbSize);
cbReflowInput->SetComputedLogicalMargin(
cbWM, cbFrame->GetLogicalUsedMargin(cbWM));
cbReflowInput->SetComputedLogicalPadding(
cbWM, cbFrame->GetLogicalUsedPadding(cbWM));
cbReflowInput->SetComputedLogicalBorderPadding(
cbWM, cbFrame->GetLogicalUsedBorderAndPadding(cbWM));
parentReflowInput.mCBReflowInput = cbReflowInput.ptr();
}
NS_WARNING_ASSERTION(parentSize.ISize(parentWM) != NS_UNCONSTRAINEDSIZE &&
parentSize.BSize(parentWM) != NS_UNCONSTRAINEDSIZE,
"parentSize should be valid");
parentReflowInput.SetComputedISize(std::max(parentSize.ISize(parentWM), 0));
parentReflowInput.SetComputedBSize(std::max(parentSize.BSize(parentWM), 0));
parentReflowInput.SetComputedLogicalMargin(parentWM, LogicalMargin(parentWM));
parentReflowInput.SetComputedLogicalPadding(
parentWM, parentFrame->GetLogicalUsedPadding(parentWM));
parentReflowInput.SetComputedLogicalBorderPadding(
parentWM, parentFrame->GetLogicalUsedBorderAndPadding(parentWM));
LogicalSize availSize = parentSize.ConvertTo(frameWM, parentWM);
availSize.BSize(frameWM) = NS_UNCONSTRAINEDSIZE;
ViewportFrame* viewport = do_QueryFrame(parentFrame);
nsSize cbSize =
viewport
? viewport->AdjustReflowInputAsContainingBlock(&parentReflowInput)
.Size()
: aFrame->GetContainingBlock()->GetSize();
const nsMargin& parentBorder =
parentReflowInput.mStyleBorder->GetComputedBorder();
cbSize -= nsSize(parentBorder.LeftRight(), parentBorder.TopBottom());
LogicalSize lcbSize(frameWM, cbSize);
ReflowInput reflowInput(aFrame->PresContext(), parentReflowInput, aFrame,
availSize, Some(lcbSize));
nscoord computedISize = reflowInput.ComputedISize();
nscoord computedBSize = reflowInput.ComputedBSize();
const auto frameBP = reflowInput.ComputedLogicalBorderPadding(frameWM);
computedISize += frameBP.IStartEnd(frameWM);
if (computedBSize != NS_UNCONSTRAINEDSIZE) {
computedBSize += frameBP.BStartEnd(frameWM);
}
LogicalSize logicalSize = aFrame->GetLogicalSize(frameWM);
nsSize size = aFrame->GetSize();
// The RecomputePosition hint is not used if any offset changed between auto
// and non-auto. If computedSize.height == NS_UNCONSTRAINEDSIZE then the new
// element height will be its intrinsic height, and since 'top' and 'bottom''s
// auto-ness hasn't changed, the old height must also be its intrinsic
// height, which we can assume hasn't changed (or reflow would have
// been triggered).
if (computedISize == logicalSize.ISize(frameWM) &&
(computedBSize == NS_UNCONSTRAINEDSIZE ||
computedBSize == logicalSize.BSize(frameWM))) {
// If we're solving for 'left' or 'top', then compute it here, in order to
// match the reflow code path.
//
// TODO(emilio): It'd be nice if this did logical math instead, but it seems
// to me the math should work out on vertical writing modes as well. See Bug
// 1675861 for some hints.
const nsMargin offset = reflowInput.ComputedPhysicalOffsets();
const nsMargin margin = reflowInput.ComputedPhysicalMargin();
nscoord left = offset.left;
if (left == NS_AUTOOFFSET) {
left =
cbSize.width - offset.right - margin.right - size.width - margin.left;
}
nscoord top = offset.top;
if (top == NS_AUTOOFFSET) {
top = cbSize.height - offset.bottom - margin.bottom - size.height -
margin.top;
}
// Move the frame
nsPoint pos(parentBorder.left + left + margin.left,
parentBorder.top + top + margin.top);
aFrame->SetPosition(pos);
postPendingScrollAnchorOrResnap(aFrame);
return true;
}
// Fall back to a reflow
return false;
}
/**
* Return true if aFrame's subtree has placeholders for out-of-flow content
* that would be affected due to the change to
* `aPossiblyChangingContainingBlock` (and thus would need to get reframed).
*
* In particular, this function returns true if there are placeholders whose OOF
* frames may need to be reparented (via reframing) as a result of whatever
* change actually happened.
*
* The `aIs{Abs,Fixed}PosContainingBlock` params represent whether
* `aPossiblyChangingContainingBlock` is a containing block for abs pos / fixed
* pos stuff, respectively, for the _new_ style that the frame already has, not
* the old one.
*/
static bool ContainingBlockChangeAffectsDescendants(
nsIFrame* aPossiblyChangingContainingBlock, nsIFrame* aFrame,
bool aIsAbsPosContainingBlock, bool aIsFixedPosContainingBlock) {
// All fixed-pos containing blocks should also be abs-pos containing blocks.
MOZ_ASSERT_IF(aIsFixedPosContainingBlock, aIsAbsPosContainingBlock);
for (const auto& childList : aFrame->ChildLists()) {
for (nsIFrame* f : childList.mList) {
if (f->IsPlaceholderFrame()) {
nsIFrame* outOfFlow = nsPlaceholderFrame::GetRealFrameForPlaceholder(f);
// If SVG text frames could appear here, they could confuse us since
// they ignore their position style ... but they can't.
NS_ASSERTION(!outOfFlow->IsInSVGTextSubtree(),
"SVG text frames can't be out of flow");
// Top-layer frames don't change containing block based on direct
// ancestors.
auto* display = outOfFlow->StyleDisplay();
if (display->IsAbsolutelyPositionedStyle() &&
display->mTopLayer == StyleTopLayer::None) {
const bool isContainingBlock =
aIsFixedPosContainingBlock ||
(aIsAbsPosContainingBlock &&
display->mPosition == StylePositionProperty::Absolute);
// NOTE(emilio): aPossiblyChangingContainingBlock is guaranteed to be
// a first continuation, see the assertion in the caller.
nsIFrame* parent = outOfFlow->GetParent()->FirstContinuation();
if (isContainingBlock) {
// If we are becoming a containing block, we only need to reframe if
// this oof's current containing block is an ancestor of the new
// frame.
if (parent != aPossiblyChangingContainingBlock &&
nsLayoutUtils::IsProperAncestorFrame(
parent, aPossiblyChangingContainingBlock)) {
return true;
}
} else {
// If we are not a containing block anymore, we only need to reframe
// if we are the current containing block of the oof frame.
if (parent == aPossiblyChangingContainingBlock) {
return true;
}
}
}
}
// NOTE: It's tempting to check f->IsAbsPosContainingBlock() or
// f->IsFixedPosContainingBlock() here. However, that would only
// be testing the *new* style of the frame, which might exclude
// descendants that currently have this frame as an abs-pos
// containing block. Taking the codepath where we don't reframe
// could lead to an unsafe call to
// cont->MarkAsNotAbsoluteContainingBlock() before we've reframed
// the descendant and taken it off the absolute list.
if (ContainingBlockChangeAffectsDescendants(
aPossiblyChangingContainingBlock, f, aIsAbsPosContainingBlock,
aIsFixedPosContainingBlock)) {
return true;
}
}
}
return false;
}
// Returns the frame that would serve as the containing block for aFrame's
// positioned descendants, if aFrame had styles to make it a CB for such
// descendants. (Typically this is just aFrame itself, or its insertion frame).
//
// Returns nullptr if this frame can't be easily determined.
static nsIFrame* ContainingBlockForFrame(nsIFrame* aFrame) {
if (aFrame->IsFieldSetFrame()) {
// FIXME: This should be easily implementable.
return nullptr;
}
nsIFrame* insertionFrame = aFrame->GetContentInsertionFrame();
if (insertionFrame == aFrame) {
return insertionFrame;
}
// Generally frames with a different insertion frame are hard to deal with,
// but scrollframes are easy because the containing block is just the
// insertion frame.
if (aFrame->IsScrollContainerFrame()) {
return insertionFrame;
}
// Combobox frames are easy as well because they can't have positioned
// children anyways.
// Button and table cell frames are also easy because the containing block is
// the frame itself.
if (aFrame->IsComboboxControlFrame() || aFrame->IsHTMLButtonControlFrame() ||
aFrame->IsTableCellFrame()) {
return aFrame;
}
return nullptr;
}
static bool NeedToReframeToUpdateContainingBlock(nsIFrame* aFrame,
nsIFrame* aMaybeChangingCB) {
// NOTE: This looks at the new style.
const bool isFixedContainingBlock = aFrame->IsFixedPosContainingBlock();
MOZ_ASSERT_IF(isFixedContainingBlock, aFrame->IsAbsPosContainingBlock());
const bool isAbsPosContainingBlock =
isFixedContainingBlock || aFrame->IsAbsPosContainingBlock();
for (nsIFrame* f = aFrame; f;
f = nsLayoutUtils::GetNextContinuationOrIBSplitSibling(f)) {
if (ContainingBlockChangeAffectsDescendants(aMaybeChangingCB, f,
isAbsPosContainingBlock,
isFixedContainingBlock)) {
return true;
}
}
return false;
}
static void DoApplyRenderingChangeToTree(nsIFrame* aFrame,
nsChangeHint aChange) {
MOZ_ASSERT(gInApplyRenderingChangeToTree,
"should only be called within ApplyRenderingChangeToTree");
for (; aFrame;
aFrame = nsLayoutUtils::GetNextContinuationOrIBSplitSibling(aFrame)) {
// Invalidate and sync views on all descendant frames, following
// placeholders. We don't need to update transforms in
// SyncViewsAndInvalidateDescendants, because there can't be any
// out-of-flows or popups that need to be transformed; all out-of-flow
// descendants of the transformed element must also be descendants of the
// transformed frame.
SyncViewsAndInvalidateDescendants(
aFrame, nsChangeHint(aChange & (nsChangeHint_RepaintFrame |
nsChangeHint_UpdateOpacityLayer |
nsChangeHint_SchedulePaint)));
// This must be set to true if the rendering change needs to
// invalidate content. If it's false, a composite-only paint
// (empty transaction) will be scheduled.
bool needInvalidatingPaint = false;
// if frame has view, will already be invalidated
if (aChange & nsChangeHint_RepaintFrame) {
// Note that this whole block will be skipped when painting is suppressed
// (due to our caller ApplyRendingChangeToTree() discarding the
// nsChangeHint_RepaintFrame hint). If you add handling for any other
// hints within this block, be sure that they too should be ignored when
// painting is suppressed.
needInvalidatingPaint = true;
aFrame->InvalidateFrameSubtree();
if ((aChange & nsChangeHint_UpdateEffects) &&
aFrame->HasAnyStateBits(NS_FRAME_SVG_LAYOUT)) {
// Need to update our overflow rects:
SVGUtils::ScheduleReflowSVG(aFrame);
}
ActiveLayerTracker::NotifyNeedsRepaint(aFrame);
}
if (aChange & nsChangeHint_UpdateOpacityLayer) {
// FIXME/bug 796697: we can get away with empty transactions for
// opacity updates in many cases.
needInvalidatingPaint = true;
ActiveLayerTracker::NotifyRestyle(aFrame, eCSSProperty_opacity);
if (SVGIntegrationUtils::UsingEffectsForFrame(aFrame)) {
// SVG effects paints the opacity without using
// nsDisplayOpacity. We need to invalidate manually.
aFrame->InvalidateFrameSubtree();
}
}
if ((aChange & nsChangeHint_UpdateTransformLayer) &&
aFrame->IsTransformed()) {
// Note: All the transform-like properties should map to the same
// layer activity index, so does the restyle count. Therefore, using
// eCSSProperty_transform should be fine.
ActiveLayerTracker::NotifyRestyle(aFrame, eCSSProperty_transform);
needInvalidatingPaint = true;
}
if (aChange & nsChangeHint_ChildrenOnlyTransform) {
needInvalidatingPaint = true;
nsIFrame* childFrame = GetFrameForChildrenOnlyTransformHint(aFrame)
->PrincipalChildList()
.FirstChild();
for (; childFrame; childFrame = childFrame->GetNextSibling()) {
// Note: All the transform-like properties should map to the same
// layer activity index, so does the restyle count. Therefore, using
// eCSSProperty_transform should be fine.
ActiveLayerTracker::NotifyRestyle(childFrame, eCSSProperty_transform);
}
}
if (aChange & nsChangeHint_SchedulePaint) {
needInvalidatingPaint = true;
}
aFrame->SchedulePaint(needInvalidatingPaint
? nsIFrame::PAINT_DEFAULT
: nsIFrame::PAINT_COMPOSITE_ONLY);
}
}
static void SyncViewsAndInvalidateDescendants(nsIFrame* aFrame,
nsChangeHint aChange) {
MOZ_ASSERT(gInApplyRenderingChangeToTree,
"should only be called within ApplyRenderingChangeToTree");
NS_ASSERTION(nsChangeHint_size_t(aChange) ==
(aChange & (nsChangeHint_RepaintFrame |
nsChangeHint_UpdateOpacityLayer |
nsChangeHint_SchedulePaint)),
"Invalid change flag");
aFrame->SyncFrameViewProperties();
for (const auto& [list, listID] : aFrame->ChildLists()) {
for (nsIFrame* child : list) {
if (!child->HasAnyStateBits(NS_FRAME_OUT_OF_FLOW)) {
// only do frames that don't have placeholders
if (child->IsPlaceholderFrame()) {
// do the out-of-flow frame and its continuations
nsIFrame* outOfFlowFrame =
nsPlaceholderFrame::GetRealFrameForPlaceholder(child);
DoApplyRenderingChangeToTree(outOfFlowFrame, aChange);
} else { // regular frame
SyncViewsAndInvalidateDescendants(child, aChange);
}
}
}
}
}
static void ApplyRenderingChangeToTree(PresShell* aPresShell, nsIFrame* aFrame,
nsChangeHint aChange) {
// We check StyleDisplay()->HasTransformStyle() in addition to checking
// IsTransformed() since we can get here for some frames that don't support
// CSS transforms, and table frames, which are their own odd-ball, since the
// transform is handled by their wrapper, which _also_ gets a separate hint.
NS_ASSERTION(!(aChange & nsChangeHint_UpdateTransformLayer) ||
aFrame->IsTransformed() ||
aFrame->StyleDisplay()->HasTransformStyle(),
"Unexpected UpdateTransformLayer hint");
if (aPresShell->IsPaintingSuppressed()) {
// Don't allow synchronous rendering changes when painting is turned off.
aChange &= ~nsChangeHint_RepaintFrame;
if (!aChange) {
return;
}
}
// Trigger rendering updates by damaging this frame and any
// continuations of this frame.
#ifdef DEBUG
gInApplyRenderingChangeToTree = true;
#endif
if (aChange & nsChangeHint_RepaintFrame) {
// If the frame is the primary frame of either the body element or
// the html element, we propagate the repaint change hint to the
// viewport. This is necessary for background and scrollbar colors
// propagation.
if (aFrame->IsPrimaryFrameOfRootOrBodyElement()) {
nsIFrame* rootFrame = aPresShell->GetRootFrame();
MOZ_ASSERT(rootFrame, "No root frame?");
DoApplyRenderingChangeToTree(rootFrame, nsChangeHint_RepaintFrame);
aChange &= ~nsChangeHint_RepaintFrame;
if (!aChange) {
return;
}
}
}
DoApplyRenderingChangeToTree(aFrame, aChange);
#ifdef DEBUG
gInApplyRenderingChangeToTree = false;
#endif
}
static void AddSubtreeToOverflowTracker(
nsIFrame* aFrame, OverflowChangedTracker& aOverflowChangedTracker) {
if (aFrame->FrameMaintainsOverflow()) {
aOverflowChangedTracker.AddFrame(aFrame,
OverflowChangedTracker::CHILDREN_CHANGED);
}
for (const auto& childList : aFrame->ChildLists()) {
for (nsIFrame* child : childList.mList) {
AddSubtreeToOverflowTracker(child, aOverflowChangedTracker);
}
}
}
static void StyleChangeReflow(nsIFrame* aFrame, nsChangeHint aHint) {
IntrinsicDirty dirtyType;
if (aHint & nsChangeHint_ClearDescendantIntrinsics) {
NS_ASSERTION(aHint & nsChangeHint_ClearAncestorIntrinsics,
"Please read the comments in nsChangeHint.h");
NS_ASSERTION(aHint & nsChangeHint_NeedDirtyReflow,
"ClearDescendantIntrinsics requires NeedDirtyReflow");
dirtyType = IntrinsicDirty::FrameAncestorsAndDescendants;
} else if ((aHint & nsChangeHint_UpdateComputedBSize) &&
aFrame->HasAnyStateBits(
NS_FRAME_DESCENDANT_INTRINSIC_ISIZE_DEPENDS_ON_BSIZE)) {
dirtyType = IntrinsicDirty::FrameAncestorsAndDescendants;
} else if (aHint & nsChangeHint_ClearAncestorIntrinsics) {
dirtyType = IntrinsicDirty::FrameAndAncestors;
} else {
dirtyType = IntrinsicDirty::None;
}
if (aHint & nsChangeHint_UpdateComputedBSize) {
aFrame->SetHasBSizeChange(true);
}
nsFrameState dirtyBits;
if (aFrame->HasAnyStateBits(NS_FRAME_FIRST_REFLOW)) {
dirtyBits = nsFrameState(0);
} else if ((aHint & nsChangeHint_NeedDirtyReflow) ||
dirtyType == IntrinsicDirty::FrameAncestorsAndDescendants) {
dirtyBits = NS_FRAME_IS_DIRTY;
} else {
dirtyBits = NS_FRAME_HAS_DIRTY_CHILDREN;
}
// If we're not going to clear any intrinsic sizes on the frames, and
// there are no dirty bits to set, then there's nothing to do.
if (dirtyType == IntrinsicDirty::None && !dirtyBits) return;
ReflowRootHandling rootHandling;
if (aHint & nsChangeHint_ReflowChangesSizeOrPosition) {
rootHandling = ReflowRootHandling::PositionOrSizeChange;
} else {
rootHandling = ReflowRootHandling::NoPositionOrSizeChange;
}
do {
aFrame->PresShell()->FrameNeedsReflow(aFrame, dirtyType, dirtyBits,
rootHandling);
aFrame = nsLayoutUtils::GetNextContinuationOrIBSplitSibling(aFrame);
} while (aFrame);
}
// Get the next sibling which might have a frame. This only considers siblings
// that stylo post-traversal looks at, so only elements and text. In
// particular, it ignores comments.
static nsIContent* NextSiblingWhichMayHaveFrame(nsIContent* aContent) {
for (nsIContent* next = aContent->GetNextSibling(); next;
next = next->GetNextSibling()) {
if (next->IsElement() || next->IsText()) {
return next;
}
}
return nullptr;
}
// If |aFrame| is dirty or has dirty children, then we can skip updating
// overflows since that will happen when it's reflowed.
static inline bool CanSkipOverflowUpdates(const nsIFrame* aFrame) {
return aFrame->HasAnyStateBits(NS_FRAME_IS_DIRTY |
NS_FRAME_HAS_DIRTY_CHILDREN);
}
static inline void TryToDealWithScrollbarChange(nsChangeHint& aHint,
nsIContent* aContent,
nsIFrame* aFrame,
nsPresContext* aPc) {
if (!(aHint & nsChangeHint_ScrollbarChange)) {
return;
}
aHint &= ~nsChangeHint_ScrollbarChange;
if (aHint & nsChangeHint_ReconstructFrame) {
return;
}
MOZ_ASSERT(aFrame, "If we're not reframing, we ought to have a frame");
const bool isRoot = aContent->IsInUncomposedDoc() && !aContent->GetParent();
// Only bother with this if we're the root or the body element, since:
// (a) It'd be *expensive* to reframe these particular nodes. They're
// at the root, so reframing would mean rebuilding the world.
// (b) It's often *unnecessary* to reframe for "overflow" changes on
// these particular nodes. In general, the only reason we reframe
// for "overflow" changes is so we can construct (or destroy) a
// scrollframe & scrollbars -- and the html/body nodes often don't
// need their own scrollframe/scrollbars because they coopt the ones
// on the viewport (which always exist). So depending on whether
// that's happening, we can skip the reframe for these nodes.
if (isRoot || aContent->IsHTMLElement(nsGkAtoms::body)) {
// If the restyled element provided/provides the scrollbar styles for
// the viewport before and/or after this restyle, AND it's not coopting
// that responsibility from some other element (which would need
// reconstruction to make its own scrollframe now), THEN: we don't need
// to reconstruct - we can just reflow, because no scrollframe is being
// added/removed.
Element* prevOverride = aPc->GetViewportScrollStylesOverrideElement();
Element* newOverride = aPc->UpdateViewportScrollStylesOverride();
const auto ProvidesScrollbarStyles = [&](nsIContent* aOverride) {
if (aOverride) {
return aOverride == aContent;
}
return isRoot;
};
if (ProvidesScrollbarStyles(prevOverride) ||
ProvidesScrollbarStyles(newOverride)) {
// If we get here, the restyled element provided the scrollbar styles
// for viewport before this restyle, OR it will provide them after.
if (!prevOverride || !newOverride || prevOverride == newOverride) {
// If we get here, the restyled element is NOT replacing (or being
// replaced by) some other element as the viewport's
// scrollbar-styles provider. (If it were, we'd potentially need to
// reframe to create a dedicated scrollframe for whichever element
// is being booted from providing viewport scrollbar styles.)
//
// Under these conditions, we're OK to assume that this "overflow"
// change only impacts the root viewport's scrollframe, which
// already exists, so we can simply reflow instead of reframing.
if (ScrollContainerFrame* sf = do_QueryFrame(aFrame)) {
sf->MarkScrollbarsDirtyForReflow();
} else if (ScrollContainerFrame* sf =
aPc->PresShell()->GetRootScrollContainerFrame()) {
sf->MarkScrollbarsDirtyForReflow();
}
aHint |= nsChangeHint_ReflowHintsForScrollbarChange;
} else {
// If we changed the override element, we need to reconstruct as the old
// override element might start / stop being scrollable.
aHint |= nsChangeHint_ReconstructFrame;
}
return;
}
}
const bool scrollable = aFrame->StyleDisplay()->IsScrollableOverflow();
if (ScrollContainerFrame* sf = do_QueryFrame(aFrame)) {
if (scrollable && sf->HasAllNeededScrollbars()) {
sf->MarkScrollbarsDirtyForReflow();
// Once we've created scrollbars for a frame, don't bother reconstructing
// it just to remove them if we still need a scroll frame.
aHint |= nsChangeHint_ReflowHintsForScrollbarChange;
return;
}
} else if (aFrame->IsTextInputFrame()) {
// input / textarea for the most part don't honor overflow themselves, the
// editor root will deal with the change if needed.
// However the textarea intrinsic size relies on GetDesiredScrollbarSizes(),
// so we need to reflow the textarea itself, not just the inner control.
aHint |= nsChangeHint_ReflowHintsForScrollbarChange;
return;
} else if (!scrollable) {
// Something changed, but we don't have nor will have a scroll frame,
// there's nothing to do here.
return;
}
// Oh well, we couldn't optimize it out, just reconstruct frames for the
// subtree.
aHint |= nsChangeHint_ReconstructFrame;
}
static void TryToHandleContainingBlockChange(nsChangeHint& aHint,
nsIFrame* aFrame) {
if (!(aHint & nsChangeHint_UpdateContainingBlock)) {
return;
}
if (aHint & nsChangeHint_ReconstructFrame) {
return;
}
MOZ_ASSERT(aFrame, "If we're not reframing, we ought to have a frame");
nsIFrame* containingBlock = ContainingBlockForFrame(aFrame);
if (!containingBlock ||
NeedToReframeToUpdateContainingBlock(aFrame, containingBlock)) {
// The frame has positioned children that need to be reparented, or it can't
// easily be converted to/from being an abs-pos container correctly.
aHint |= nsChangeHint_ReconstructFrame;
return;
}
const bool isCb = aFrame->IsAbsPosContainingBlock();
// The absolute container should be containingBlock.
for (nsIFrame* cont = containingBlock; cont;
cont = nsLayoutUtils::GetNextContinuationOrIBSplitSibling(cont)) {
// Normally frame construction would set state bits as needed,
// but we're not going to reconstruct the frame so we need to set
// them. It's because we need to set this state on each affected frame
// that we can't coalesce nsChangeHint_UpdateContainingBlock hints up
// to ancestors (i.e. it can't be an change hint that is handled for
// descendants).
if (isCb) {
if (!cont->IsAbsoluteContainer() &&
cont->HasAnyStateBits(NS_FRAME_CAN_HAVE_ABSPOS_CHILDREN)) {
cont->MarkAsAbsoluteContainingBlock();
}
} else if (cont->IsAbsoluteContainer()) {
if (cont->HasAbsolutelyPositionedChildren()) {
// If |cont| still has absolutely positioned children,
// we can't call MarkAsNotAbsoluteContainingBlock. This
// will remove a frame list that still has children in
// it that we need to keep track of.
// The optimization of removing it isn't particularly
// important, although it does mean we skip some tests.
NS_WARNING("skipping removal of absolute containing block");
} else {
cont->MarkAsNotAbsoluteContainingBlock();
}
}
}
}
void RestyleManager::ProcessRestyledFrames(nsStyleChangeList& aChangeList) {
NS_ASSERTION(!nsContentUtils::IsSafeToRunScript(),
"Someone forgot a script blocker");
// See bug 1378219 comment 9:
// Recursive calls here are a bit worrying, but apparently do happen in the
// wild (although not currently in any of our automated tests). Try to get a
// stack from Nightly/Dev channel to figure out what's going on and whether
// it's OK.
MOZ_DIAGNOSTIC_ASSERT(!mDestroyedFrames, "ProcessRestyledFrames recursion");
if (aChangeList.IsEmpty()) {
return;
}
// If mDestroyedFrames is null, we want to create a new hashtable here
// and destroy it on exit; but if it is already non-null (because we're in
// a recursive call), we will continue to use the existing table to
// accumulate destroyed frames, and NOT clear mDestroyedFrames on exit.
// We use a MaybeClearDestroyedFrames helper to conditionally reset the
// mDestroyedFrames pointer when this method returns.
typedef decltype(mDestroyedFrames) DestroyedFramesT;
class MOZ_RAII MaybeClearDestroyedFrames {
private:
DestroyedFramesT& mDestroyedFramesRef; // ref to caller's mDestroyedFrames
const bool mResetOnDestruction;
public:
explicit MaybeClearDestroyedFrames(DestroyedFramesT& aTarget)
: mDestroyedFramesRef(aTarget),
mResetOnDestruction(!aTarget) // reset only if target starts out null
{}
~MaybeClearDestroyedFrames() {
if (mResetOnDestruction) {
mDestroyedFramesRef.reset(nullptr);
}
}
};
MaybeClearDestroyedFrames maybeClear(mDestroyedFrames);
if (!mDestroyedFrames) {
mDestroyedFrames = MakeUnique<nsTHashSet<const nsIFrame*>>();
}
AUTO_PROFILER_LABEL("RestyleManager::ProcessRestyledFrames", LAYOUT);
nsPresContext* presContext = PresContext();
nsCSSFrameConstructor* frameConstructor = presContext->FrameConstructor();
bool didUpdateCursor = false;
for (size_t i = 0; i < aChangeList.Length(); ++i) {
// Collect and coalesce adjacent siblings for lazy frame construction.
// Eventually it would be even better to make RecreateFramesForContent
// accept a range and coalesce all adjacent reconstructs (bug 1344139).
size_t lazyRangeStart = i;
while (i < aChangeList.Length() && aChangeList[i].mContent &&
aChangeList[i].mContent->HasFlag(NODE_NEEDS_FRAME) &&
(i == lazyRangeStart ||
NextSiblingWhichMayHaveFrame(aChangeList[i - 1].mContent) ==
aChangeList[i].mContent)) {
MOZ_ASSERT(aChangeList[i].mHint & nsChangeHint_ReconstructFrame);
MOZ_ASSERT(!aChangeList[i].mFrame);
++i;
}
if (i != lazyRangeStart) {
nsIContent* start = aChangeList[lazyRangeStart].mContent;
nsIContent* end =
NextSiblingWhichMayHaveFrame(aChangeList[i - 1].mContent);
if (!end) {
frameConstructor->ContentAppended(
start, nsCSSFrameConstructor::InsertionKind::Sync);
} else {
frameConstructor->ContentRangeInserted(
start, end, nsCSSFrameConstructor::InsertionKind::Sync);
}
}
for (size_t j = lazyRangeStart; j < i; ++j) {
MOZ_ASSERT(!aChangeList[j].mContent->GetPrimaryFrame() ||
!aChangeList[j].mContent->HasFlag(NODE_NEEDS_FRAME));
}
if (i == aChangeList.Length()) {
break;
}
const nsStyleChangeData& data = aChangeList[i];
nsIFrame* frame = data.mFrame;
nsIContent* content = data.mContent;
nsChangeHint hint = data.mHint;
bool didReflowThisFrame = false;
NS_ASSERTION(!(hint & nsChangeHint_AllReflowHints) ||
(hint & nsChangeHint_NeedReflow),
"Reflow hint bits set without actually asking for a reflow");
// skip any frame that has been destroyed due to a ripple effect
if (frame && mDestroyedFrames->Contains(frame)) {
continue;
}
if (frame && frame->GetContent() != content) {
// XXXbz this is due to image maps messing with the primary frame of
// <area>s. See bug 135040. Remove this block once that's fixed.
frame = nullptr;
if (!(hint & nsChangeHint_ReconstructFrame)) {
continue;
}
}
TryToDealWithScrollbarChange(hint, content, frame, presContext);
TryToHandleContainingBlockChange(hint, frame);
if (hint & nsChangeHint_ReconstructFrame) {
// If we ever start passing true here, be careful of restyles
// that involve a reframe and animations. In particular, if the
// restyle we're processing here is an animation restyle, but
// the style resolution we will do for the frame construction
// happens async when we're not in an animation restyle already,
// problems could arise.
// We could also have problems with triggering of CSS transitions
// on elements whose frames are reconstructed, since we depend on
// the reconstruction happening synchronously.
frameConstructor->RecreateFramesForContent(
content, nsCSSFrameConstructor::InsertionKind::Sync);
continue;
}
MOZ_ASSERT(frame, "This shouldn't happen");
if (hint & nsChangeHint_AddOrRemoveTransform) {
for (nsIFrame* cont = frame; cont;
cont = nsLayoutUtils::GetNextContinuationOrIBSplitSibling(cont)) {
if (cont->StyleDisplay()->HasTransform(cont)) {
cont->AddStateBits(NS_FRAME_MAY_BE_TRANSFORMED);
}
// Don't remove NS_FRAME_MAY_BE_TRANSFORMED since it may still be
// transformed by other means. It's OK to have the bit even if it's
// not needed.
}
// When dropping a running transform animation we will first add an
// nsChangeHint_UpdateTransformLayer hint as part of the animation-only
// restyle. During the subsequent regular restyle, if the animation was
// the only reason the element had any transform applied, we will add
// nsChangeHint_AddOrRemoveTransform as part of the regular restyle.
//
// With the Gecko backend, these two change hints are processed
// after each restyle but when using the Servo backend they accumulate
// and are processed together after we have already removed the
// transform as part of the regular restyle. Since we don't actually
// need the nsChangeHint_UpdateTransformLayer hint if we already have
// a nsChangeHint_AddOrRemoveTransform hint, and since we
// will fail an assertion in ApplyRenderingChangeToTree if we try
// specify nsChangeHint_UpdateTransformLayer but don't have any
// transform style, we just drop the unneeded hint here.
hint &= ~nsChangeHint_UpdateTransformLayer;
}
if (!frame->FrameMaintainsOverflow()) {
// frame does not maintain overflow rects, so avoid calling
// FinishAndStoreOverflow on it:
hint &=
~(nsChangeHint_UpdateOverflow | nsChangeHint_ChildrenOnlyTransform |
nsChangeHint_UpdatePostTransformOverflow |
nsChangeHint_UpdateParentOverflow |
nsChangeHint_UpdateSubtreeOverflow);
}
if (!frame->HasAnyStateBits(NS_FRAME_MAY_BE_TRANSFORMED)) {
// Frame can not be transformed, and thus a change in transform will
// have no effect and we should not use either
// nsChangeHint_UpdatePostTransformOverflow or
// nsChangeHint_UpdateTransformLayerhint.
hint &= ~(nsChangeHint_UpdatePostTransformOverflow |
nsChangeHint_UpdateTransformLayer);
}
if ((hint & nsChangeHint_UpdateEffects) &&
frame == nsLayoutUtils::FirstContinuationOrIBSplitSibling(frame)) {
SVGObserverUtils::UpdateEffects(frame);
}
if ((hint & nsChangeHint_InvalidateRenderingObservers) ||
((hint & nsChangeHint_UpdateOpacityLayer) &&
frame->HasAnyStateBits(NS_FRAME_SVG_LAYOUT))) {
SVGObserverUtils::InvalidateRenderingObservers(frame);
frame->SchedulePaint();
}
if (hint & nsChangeHint_NeedReflow) {
StyleChangeReflow(frame, hint);
didReflowThisFrame = true;
}
// Here we need to propagate repaint frame change hint instead of update
<