1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* vim: set ts=8 sts=2 et sw=2 tw=80: */
3 /* This Source Code Form is subject to the terms of the Mozilla Public
4 * License, v. 2.0. If a copy of the MPL was not distributed with this
5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
7 #include "SMILAnimationController.h"
11 #include "mozilla/AutoRestore.h"
12 #include "mozilla/PresShell.h"
13 #include "mozilla/PresShellInlines.h"
14 #include "mozilla/RestyleManager.h"
15 #include "mozilla/SMILTimedElement.h"
16 #include "mozilla/dom/DocumentInlines.h"
17 #include "mozilla/dom/Element.h"
18 #include "mozilla/dom/SVGAnimationElement.h"
19 #include "nsContentUtils.h"
20 #include "nsCSSProps.h"
21 #include "nsRefreshDriver.h"
22 #include "mozilla/dom/Document.h"
23 #include "SMILCompositor.h"
24 #include "SMILCSSProperty.h"
26 using namespace mozilla::dom
;
30 //----------------------------------------------------------------------
31 // SMILAnimationController implementation
33 //----------------------------------------------------------------------
34 // ctors, dtors, factory methods
36 SMILAnimationController::SMILAnimationController(Document
* aDoc
)
38 MOZ_ASSERT(aDoc
, "need a non-null document");
40 if (nsRefreshDriver
* refreshDriver
= GetRefreshDriver()) {
41 mStartTime
= refreshDriver
->MostRecentRefresh();
43 mStartTime
= mozilla::TimeStamp::Now();
45 mCurrentSampleTime
= mStartTime
;
50 SMILAnimationController::~SMILAnimationController() {
51 NS_ASSERTION(mAnimationElementTable
.IsEmpty(),
52 "Animation controller shouldn't be tracking any animation"
53 " elements when it dies");
54 MOZ_RELEASE_ASSERT(!mRegisteredWithRefreshDriver
,
55 "Leaving stale entry in refresh driver's observer list");
58 void SMILAnimationController::Disconnect() {
59 MOZ_ASSERT(mDocument
, "disconnecting when we weren't connected...?");
60 MOZ_ASSERT(mRefCnt
.get() == 1,
61 "Expecting to disconnect when doc is sole remaining owner");
62 NS_ASSERTION(mPauseState
& SMILTimeContainer::PAUSE_PAGEHIDE
,
63 "Expecting to be paused for pagehide before disconnect");
65 StopSampling(GetRefreshDriver());
67 mDocument
= nullptr; // (raw pointer)
70 //----------------------------------------------------------------------
71 // SMILTimeContainer methods:
73 void SMILAnimationController::Pause(uint32_t aType
) {
74 SMILTimeContainer::Pause(aType
);
78 void SMILAnimationController::Resume(uint32_t aType
) {
79 bool wasPaused
= !!mPauseState
;
80 // Update mCurrentSampleTime so that calls to GetParentTime--used for
81 // calculating parent offsets--are accurate
82 mCurrentSampleTime
= mozilla::TimeStamp::Now();
84 SMILTimeContainer::Resume(aType
);
86 if (wasPaused
&& !mPauseState
) {
91 SMILTime
SMILAnimationController::GetParentTime() const {
92 return (SMILTime
)(mCurrentSampleTime
- mStartTime
).ToMilliseconds();
95 //----------------------------------------------------------------------
96 // nsARefreshObserver methods:
97 NS_IMPL_ADDREF(SMILAnimationController
)
98 NS_IMPL_RELEASE(SMILAnimationController
)
100 // nsRefreshDriver Callback function
101 void SMILAnimationController::WillRefresh(mozilla::TimeStamp aTime
) {
102 // Although we never expect aTime to go backwards, when we initialise the
103 // animation controller, if we can't get hold of a refresh driver we
104 // initialise mCurrentSampleTime to Now(). It may be possible that after
105 // doing so we get sampled by a refresh driver whose most recent refresh time
106 // predates when we were initialised, so to be safe we make sure to take the
107 // most recent time here.
108 aTime
= std::max(mCurrentSampleTime
, aTime
);
110 // Sleep detection: If the time between samples is a whole lot greater than we
111 // were expecting then we assume the computer went to sleep or someone's
112 // messing with the clock. In that case, fiddle our parent offset and use our
113 // average time between samples to calculate the new sample time. This
114 // prevents us from hanging while trying to catch up on all the missed time.
116 // Smoothing of coefficient for the average function. 0.2 should let us track
117 // the sample rate reasonably tightly without being overly affected by
118 // occasional delays.
119 static const double SAMPLE_DUR_WEIGHTING
= 0.2;
120 // If the elapsed time exceeds our expectation by this number of times we'll
121 // initiate special behaviour to basically ignore the intervening time.
122 static const double SAMPLE_DEV_THRESHOLD
= 200.0;
124 SMILTime elapsedTime
=
125 (SMILTime
)(aTime
- mCurrentSampleTime
).ToMilliseconds();
126 if (mAvgTimeBetweenSamples
== 0) {
128 mAvgTimeBetweenSamples
= elapsedTime
;
130 if (elapsedTime
> SAMPLE_DEV_THRESHOLD
* mAvgTimeBetweenSamples
) {
131 // Unexpectedly long delay between samples.
133 "Detected really long delay between samples, continuing from "
135 mParentOffset
+= elapsedTime
- mAvgTimeBetweenSamples
;
137 // Update the moving average. Due to truncation here the average will
138 // normally be a little less than it should be but that's probably ok.
139 mAvgTimeBetweenSamples
=
140 (SMILTime
)(elapsedTime
* SAMPLE_DUR_WEIGHTING
+
141 mAvgTimeBetweenSamples
* (1.0 - SAMPLE_DUR_WEIGHTING
));
143 mCurrentSampleTime
= aTime
;
148 //----------------------------------------------------------------------
149 // Animation element registration methods:
151 void SMILAnimationController::RegisterAnimationElement(
152 SVGAnimationElement
* aAnimationElement
) {
153 const bool wasEmpty
= mAnimationElementTable
.IsEmpty();
154 mAnimationElementTable
.PutEntry(aAnimationElement
);
160 void SMILAnimationController::UnregisterAnimationElement(
161 SVGAnimationElement
* aAnimationElement
) {
162 mAnimationElementTable
.RemoveEntry(aAnimationElement
);
163 if (mAnimationElementTable
.IsEmpty()) {
168 //----------------------------------------------------------------------
171 void SMILAnimationController::OnPageShow() {
172 Resume(SMILTimeContainer::PAUSE_PAGEHIDE
);
175 void SMILAnimationController::OnPageHide() {
176 Pause(SMILTimeContainer::PAUSE_PAGEHIDE
);
179 //----------------------------------------------------------------------
180 // Cycle-collection support
182 void SMILAnimationController::Traverse(
183 nsCycleCollectionTraversalCallback
* aCallback
) {
184 // Traverse last compositor table
185 if (mLastCompositorTable
) {
186 for (SMILCompositor
& compositor
: *mLastCompositorTable
) {
187 compositor
.Traverse(aCallback
);
192 void SMILAnimationController::Unlink() { mLastCompositorTable
= nullptr; }
194 //----------------------------------------------------------------------
195 // Refresh driver lifecycle related methods
197 void SMILAnimationController::NotifyRefreshDriverCreated(
198 nsRefreshDriver
* aRefreshDriver
) {
202 void SMILAnimationController::NotifyRefreshDriverDestroying(
203 nsRefreshDriver
* aRefreshDriver
) {
204 StopSampling(aRefreshDriver
);
207 //----------------------------------------------------------------------
208 // Timer-related implementation helpers
210 bool SMILAnimationController::ShouldSample() const {
211 return !mPauseState
&& !mAnimationElementTable
.IsEmpty() &&
212 !mChildContainerTable
.IsEmpty();
215 void SMILAnimationController::UpdateSampling() {
216 const bool shouldSample
= ShouldSample();
217 const bool isSampling
= mRegisteredWithRefreshDriver
;
218 if (shouldSample
== isSampling
) {
222 nsRefreshDriver
* driver
= GetRefreshDriver();
228 // We're effectively resuming from a pause so update our current sample time
229 // or else it will confuse our "average time between samples" calculations.
230 mCurrentSampleTime
= mozilla::TimeStamp::Now();
231 driver
->AddRefreshObserver(this, FlushType::Style
, "SMIL animations");
232 mRegisteredWithRefreshDriver
= true;
233 Sample(); // Run the first sample manually.
235 StopSampling(driver
);
239 void SMILAnimationController::StopSampling(nsRefreshDriver
* aRefreshDriver
) {
240 if (aRefreshDriver
&& mRegisteredWithRefreshDriver
) {
241 // NOTE: The document might already have been detached from its PresContext
242 // (and RefreshDriver), which would make GetRefreshDriver() return null.
243 MOZ_ASSERT(!GetRefreshDriver() || aRefreshDriver
== GetRefreshDriver(),
244 "Stopping sampling with wrong refresh driver");
245 aRefreshDriver
->RemoveRefreshObserver(this, FlushType::Style
);
246 mRegisteredWithRefreshDriver
= false;
250 //----------------------------------------------------------------------
251 // Sample-related methods and callbacks
253 void SMILAnimationController::DoSample() {
254 DoSample(true); // Skip unchanged time containers
257 void SMILAnimationController::DoSample(bool aSkipUnchangedContainers
) {
259 NS_ERROR("Shouldn't be sampling after document has disconnected");
262 if (mRunningSample
) {
263 NS_ERROR("Shouldn't be recursively sampling");
267 mResampleNeeded
= false;
269 // Set running sample flag -- do this before flushing styles so that when we
270 // flush styles we don't end up requesting extra samples
271 AutoRestore
<bool> autoRestoreRunningSample(mRunningSample
);
272 mRunningSample
= true;
274 // STEP 1: Bring model up to date
275 // (i) Rewind elements where necessary
276 // (ii) Run milestone samples
278 DoMilestoneSamples();
280 // STEP 2: Sample the child time containers
282 // When we sample the child time containers they will simply record the sample
283 // time in document time.
284 TimeContainerHashtable
activeContainers(mChildContainerTable
.Count());
285 for (SMILTimeContainer
* container
: mChildContainerTable
.Keys()) {
290 if (!container
->IsPausedByType(SMILTimeContainer::PAUSE_BEGIN
) &&
291 (container
->NeedsSample() || !aSkipUnchangedContainers
)) {
292 container
->ClearMilestones();
294 container
->MarkSeekFinished();
295 activeContainers
.PutEntry(container
);
299 // STEP 3: (i) Sample the timed elements AND
300 // (ii) Create a table of compositors
302 // (i) Here we sample the timed elements (fetched from the
303 // SVGAnimationElements) which determine from the active time if the
304 // element is active and what its simple time etc. is. This information is
305 // then passed to its time client (SMILAnimationFunction).
307 // (ii) During the same loop we also build up a table that contains one
308 // compositor for each animated attribute and which maps animated elements to
309 // the corresponding compositor for their target attribute.
311 // Note that this compositor table needs to be allocated on the heap so we can
312 // store it until the next sample. This lets us find out which elements were
313 // animated in sample 'n-1' but not in sample 'n' (and hence need to have
314 // their animation effects removed in sample 'n').
316 // Parts (i) and (ii) are not functionally related but we combine them here to
317 // save iterating over the animation elements twice.
319 // Create the compositor table
320 UniquePtr
<SMILCompositorTable
> currentCompositorTable(
321 new SMILCompositorTable(0));
322 nsTArray
<RefPtr
<SVGAnimationElement
>> animElems(
323 mAnimationElementTable
.Count());
325 for (SVGAnimationElement
* animElem
: mAnimationElementTable
.Keys()) {
326 SampleTimedElement(animElem
, &activeContainers
);
327 AddAnimationToCompositorTable(animElem
, currentCompositorTable
.get());
328 animElems
.AppendElement(animElem
);
330 activeContainers
.Clear();
332 // STEP 4: Compare previous sample's compositors against this sample's.
333 // (Transfer cached base values across, & remove animation effects from
334 // no-longer-animated targets.)
335 if (mLastCompositorTable
) {
336 // * Transfer over cached base values, from last sample's compositors
337 for (SMILCompositor
& compositor
: *currentCompositorTable
) {
338 SMILCompositor
* lastCompositor
=
339 mLastCompositorTable
->GetEntry(compositor
.GetKey());
341 if (lastCompositor
) {
342 compositor
.StealCachedBaseValue(lastCompositor
);
343 if (!lastCompositor
->HasSameNumberOfAnimationFunctionsAs(compositor
)) {
344 // If we have multiple animations on the same element, they share a
345 // compositor. If an active animation ends, it will no longer be in
346 // the compositor table. We need to force compositing to ensure we
347 // render the element with any remaining frozen animations even though
348 // they would not normally trigger compositing.
349 compositor
.ToggleForceCompositing();
354 // * For each compositor in current sample's hash table, remove entry from
355 // prev sample's hash table -- we don't need to clear animation
356 // effects of those compositors, since they're still being animated.
357 for (const auto& key
: currentCompositorTable
->Keys()) {
358 mLastCompositorTable
->RemoveEntry(key
);
361 // * For each entry that remains in prev sample's hash table (i.e. for
362 // every target that's no longer animated), clear animation effects.
363 for (SMILCompositor
& compositor
: *mLastCompositorTable
) {
364 compositor
.ClearAnimationEffects();
368 // return early if there are no active animations to avoid a style flush
369 if (currentCompositorTable
->IsEmpty()) {
370 mLastCompositorTable
= nullptr;
374 // STEP 5: Compose currently-animated attributes.
375 // XXXdholbert: This step traverses our animation targets in an effectively
376 // random order. For animation from/to 'inherit' values to work correctly
377 // when the inherited value is *also* being animated, we really should be
378 // traversing our animated nodes in an ancestors-first order (bug 501183)
379 bool mightHavePendingStyleUpdates
= false;
380 for (auto& compositor
: *currentCompositorTable
) {
381 compositor
.ComposeAttribute(mightHavePendingStyleUpdates
);
384 // Update last compositor table
385 mLastCompositorTable
= std::move(currentCompositorTable
);
386 mMightHavePendingStyleUpdates
= mightHavePendingStyleUpdates
;
388 NS_ASSERTION(!mResampleNeeded
, "Resample dirty flag set during sample!");
391 void SMILAnimationController::RewindElements() {
392 const bool rewindNeeded
= std::any_of(
393 mChildContainerTable
.Keys().cbegin(), mChildContainerTable
.Keys().cend(),
394 [](SMILTimeContainer
* container
) { return container
->NeedsRewind(); });
396 if (!rewindNeeded
) return;
398 for (SVGAnimationElement
* animElem
: mAnimationElementTable
.Keys()) {
399 SMILTimeContainer
* timeContainer
= animElem
->GetTimeContainer();
400 if (timeContainer
&& timeContainer
->NeedsRewind()) {
401 animElem
->TimedElement().Rewind();
405 for (SMILTimeContainer
* container
: mChildContainerTable
.Keys()) {
406 container
->ClearNeedsRewind();
410 void SMILAnimationController::DoMilestoneSamples() {
411 // We need to sample the timing model but because SMIL operates independently
412 // of the frame-rate, we can get one sample at t=0s and the next at t=10min.
414 // In between those two sample times a whole string of significant events
415 // might be expected to take place: events firing, new interdependencies
416 // between animations resolved and dissolved, etc.
418 // Furthermore, at any given time, we want to sample all the intervals that
419 // end at that time BEFORE any that begin. This behaviour is implied by SMIL's
420 // endpoint-exclusive timing model.
422 // So we have the animations (specifically the timed elements) register the
423 // next significant moment (called a milestone) in their lifetime and then we
424 // step through the model at each of these moments and sample those animations
425 // registered for those times. This way events can fire in the correct order,
426 // dependencies can be resolved etc.
428 SMILTime sampleTime
= INT64_MIN
;
431 // We want to find any milestones AT OR BEFORE the current sample time so we
432 // initialise the next milestone to the moment after (1ms after, to be
433 // precise) the current sample time and see if there are any milestones
434 // before that. Any other milestones will be dealt with in a subsequent
436 SMILMilestone
nextMilestone(GetCurrentTimeAsSMILTime() + 1, true);
437 for (SMILTimeContainer
* container
: mChildContainerTable
.Keys()) {
438 if (container
->IsPausedByType(SMILTimeContainer::PAUSE_BEGIN
)) {
441 SMILMilestone thisMilestone
;
442 bool didGetMilestone
=
443 container
->GetNextMilestoneInParentTime(thisMilestone
);
444 if (didGetMilestone
&& thisMilestone
< nextMilestone
) {
445 nextMilestone
= thisMilestone
;
449 if (nextMilestone
.mTime
> GetCurrentTimeAsSMILTime()) {
453 nsTArray
<RefPtr
<dom::SVGAnimationElement
>> elements
;
454 for (SMILTimeContainer
* container
: mChildContainerTable
.Keys()) {
455 if (container
->IsPausedByType(SMILTimeContainer::PAUSE_BEGIN
)) {
458 container
->PopMilestoneElementsAtMilestone(nextMilestone
, elements
);
461 // During the course of a sampling we don't want to actually go backwards.
462 // Due to negative offsets, early ends and the like, a timed element might
463 // register a milestone that is actually in the past. That's fine, but it's
464 // still only going to get *sampled* with whatever time we're up to and no
467 // Because we're only performing this clamping at the last moment, the
468 // animations will still all get sampled in the correct order and
469 // dependencies will be appropriately resolved.
470 sampleTime
= std::max(nextMilestone
.mTime
, sampleTime
);
472 for (RefPtr
<dom::SVGAnimationElement
>& elem
: elements
) {
473 MOZ_ASSERT(elem
, "nullptr animation element in list");
474 SMILTimeContainer
* container
= elem
->GetTimeContainer();
476 // The container may be nullptr if the element has been detached from
477 // its parent since registering a milestone.
480 SMILTimeValue containerTimeValue
=
481 container
->ParentToContainerTime(sampleTime
);
482 if (!containerTimeValue
.IsDefinite()) continue;
484 // Clamp the converted container time to non-negative values.
485 SMILTime containerTime
=
486 std::max
<SMILTime
>(0, containerTimeValue
.GetMillis());
488 if (nextMilestone
.mIsEnd
) {
489 elem
->TimedElement().SampleEndAt(containerTime
);
491 elem
->TimedElement().SampleAt(containerTime
);
498 void SMILAnimationController::SampleTimedElement(
499 SVGAnimationElement
* aElement
, TimeContainerHashtable
* aActiveContainers
) {
500 SMILTimeContainer
* timeContainer
= aElement
->GetTimeContainer();
501 if (!timeContainer
) return;
503 // We'd like to call timeContainer->NeedsSample() here and skip all timed
504 // elements that belong to paused time containers that don't need a sample,
505 // but that doesn't work because we've already called Sample() on all the time
506 // containers so the paused ones don't need a sample any more and they'll
509 // Instead we build up a hashmap of active time containers during the previous
510 // step (SampleTimeContainer) and then test here if the container for this
511 // timed element is in the list.
512 if (!aActiveContainers
->GetEntry(timeContainer
)) return;
514 SMILTime containerTime
= timeContainer
->GetCurrentTimeAsSMILTime();
516 MOZ_ASSERT(!timeContainer
->IsSeeking(),
517 "Doing a regular sample but the time container is still seeking");
518 aElement
->TimedElement().SampleAt(containerTime
);
522 void SMILAnimationController::AddAnimationToCompositorTable(
523 SVGAnimationElement
* aElement
, SMILCompositorTable
* aCompositorTable
) {
524 // Add a compositor to the hash table if there's not already one there
525 SMILTargetIdentifier key
;
526 if (!GetTargetIdentifierForAnimation(aElement
, key
))
527 // Something's wrong/missing about animation's target; skip this animation
530 SMILAnimationFunction
& func
= aElement
->AnimationFunction();
532 // Only add active animation functions. If there are no active animations
533 // targeting an attribute, no compositor will be created and any previously
534 // applied animations will be cleared.
535 if (func
.IsActiveOrFrozen()) {
536 // Look up the compositor for our target, & add our animation function
537 // to its list of animation functions.
538 SMILCompositor
* result
= aCompositorTable
->PutEntry(key
);
539 result
->AddAnimationFunction(&func
);
541 } else if (func
.HasChanged()) {
542 // Look up the compositor for our target, and force it to skip the
543 // "nothing's changed so don't bother compositing" optimization for this
544 // sample. |func| is inactive, but it's probably *newly* inactive (since
545 // it's got HasChanged() == true), so we need to make sure to recompose
547 SMILCompositor
* result
= aCompositorTable
->PutEntry(key
);
548 result
->ToggleForceCompositing();
550 // We've now made sure that |func|'s inactivity will be reflected as of
551 // this sample. We need to clear its HasChanged() flag so that it won't
552 // trigger this same clause in future samples (until it changes again).
553 func
.ClearHasChanged();
557 static inline bool IsTransformAttribute(int32_t aNamespaceID
,
558 nsAtom
* aAttributeName
) {
559 return aNamespaceID
== kNameSpaceID_None
&&
560 (aAttributeName
== nsGkAtoms::transform
||
561 aAttributeName
== nsGkAtoms::patternTransform
||
562 aAttributeName
== nsGkAtoms::gradientTransform
);
565 // Helper function that, given a SVGAnimationElement, looks up its target
566 // element & target attribute and populates a SMILTargetIdentifier
569 bool SMILAnimationController::GetTargetIdentifierForAnimation(
570 SVGAnimationElement
* aAnimElem
, SMILTargetIdentifier
& aResult
) {
571 // Look up target (animated) element
572 Element
* targetElem
= aAnimElem
->GetTargetElementContent();
574 // Animation has no target elem -- skip it.
577 // Look up target (animated) attribute
578 // SMILANIM section 3.1, attributeName may
579 // have an XMLNS prefix to indicate the XML namespace.
580 RefPtr
<nsAtom
> attributeName
;
581 int32_t attributeNamespaceID
;
582 if (!aAnimElem
->GetTargetAttributeName(&attributeNamespaceID
,
583 getter_AddRefs(attributeName
)))
584 // Animation has no target attr -- skip it.
587 // animateTransform can only animate transforms, conversely transforms
588 // can only be animated by animateTransform
589 if (IsTransformAttribute(attributeNamespaceID
, attributeName
) !=
590 (aAnimElem
->IsSVGElement(nsGkAtoms::animateTransform
)))
594 aResult
.mElement
= targetElem
;
595 aResult
.mAttributeName
= attributeName
;
596 aResult
.mAttributeNamespaceID
= attributeNamespaceID
;
601 void SMILAnimationController::PreTraverse() { PreTraverseInSubtree(nullptr); }
603 void SMILAnimationController::PreTraverseInSubtree(Element
* aRoot
) {
604 MOZ_ASSERT(NS_IsMainThread());
606 if (!mMightHavePendingStyleUpdates
) {
610 nsPresContext
* context
= mDocument
->GetPresContext();
615 for (SVGAnimationElement
* animElement
: mAnimationElementTable
.Keys()) {
616 SMILTargetIdentifier key
;
617 if (!GetTargetIdentifierForAnimation(animElement
, key
)) {
618 // Something's wrong/missing about animation's target; skip this animation
622 // Ignore restyles that aren't in the flattened tree subtree rooted at
624 if (aRoot
&& !nsContentUtils::ContentIsFlattenedTreeDescendantOf(
625 key
.mElement
, aRoot
)) {
629 context
->RestyleManager()->PostRestyleEventForAnimations(
630 key
.mElement
, PseudoStyleType::NotPseudo
, RestyleHint::RESTYLE_SMIL
);
633 // Only clear the mMightHavePendingStyleUpdates flag if we definitely posted
636 mMightHavePendingStyleUpdates
= false;
640 //----------------------------------------------------------------------
641 // Add/remove child time containers
643 nsresult
SMILAnimationController::AddChild(SMILTimeContainer
& aChild
) {
644 const bool wasEmpty
= mChildContainerTable
.IsEmpty();
645 TimeContainerPtrKey
* key
= mChildContainerTable
.PutEntry(&aChild
);
646 NS_ENSURE_TRUE(key
, NS_ERROR_OUT_OF_MEMORY
);
653 void SMILAnimationController::RemoveChild(SMILTimeContainer
& aChild
) {
654 mChildContainerTable
.RemoveEntry(&aChild
);
655 if (mChildContainerTable
.IsEmpty()) {
661 nsRefreshDriver
* SMILAnimationController::GetRefreshDriver() {
663 NS_ERROR("Requesting refresh driver after document has disconnected!");
667 nsPresContext
* context
= mDocument
->GetPresContext();
668 return context
? context
->RefreshDriver() : nullptr;
671 void SMILAnimationController::FlagDocumentNeedsFlush() {
672 if (PresShell
* presShell
= mDocument
->GetPresShell()) {
673 presShell
->SetNeedStyleFlush();
677 } // namespace mozilla