/src/mozilla-central/layout/style/nsTransitionManager.cpp
Line | Count | Source (jump to first uncovered line) |
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/. */ |
6 | | |
7 | | /* Code to start and animate CSS transitions. */ |
8 | | |
9 | | #include "nsTransitionManager.h" |
10 | | #include "nsAnimationManager.h" |
11 | | #include "mozilla/dom/CSSTransitionBinding.h" |
12 | | |
13 | | #include "nsIContent.h" |
14 | | #include "mozilla/ComputedStyle.h" |
15 | | #include "mozilla/MemoryReporting.h" |
16 | | #include "mozilla/TimeStamp.h" |
17 | | #include "nsRefreshDriver.h" |
18 | | #include "nsCSSPropertyIDSet.h" |
19 | | #include "mozilla/AnimationEventDispatcher.h" |
20 | | #include "mozilla/EffectCompositor.h" |
21 | | #include "mozilla/EffectSet.h" |
22 | | #include "mozilla/EventDispatcher.h" |
23 | | #include "mozilla/ServoBindings.h" |
24 | | #include "mozilla/StyleAnimationValue.h" |
25 | | #include "mozilla/dom/DocumentTimeline.h" |
26 | | #include "mozilla/dom/Element.h" |
27 | | #include "nsIFrame.h" |
28 | | #include "Layers.h" |
29 | | #include "FrameLayerBuilder.h" |
30 | | #include "nsCSSProps.h" |
31 | | #include "nsCSSPseudoElements.h" |
32 | | #include "nsDisplayList.h" |
33 | | #include "nsRFPService.h" |
34 | | #include "nsStyleChangeList.h" |
35 | | #include "mozilla/RestyleManager.h" |
36 | | #include "nsDOMMutationObserver.h" |
37 | | |
38 | | using mozilla::TimeStamp; |
39 | | using mozilla::TimeDuration; |
40 | | using mozilla::dom::Animation; |
41 | | using mozilla::dom::AnimationPlayState; |
42 | | using mozilla::dom::CSSTransition; |
43 | | using mozilla::dom::Nullable; |
44 | | |
45 | | using namespace mozilla; |
46 | | using namespace mozilla::css; |
47 | | |
48 | | double |
49 | | ElementPropertyTransition::CurrentValuePortion() const |
50 | 0 | { |
51 | 0 | MOZ_ASSERT(!GetLocalTime().IsNull(), |
52 | 0 | "Getting the value portion of an animation that's not being " |
53 | 0 | "sampled"); |
54 | 0 |
|
55 | 0 | // Transitions use a fill mode of 'backwards' so GetComputedTiming will |
56 | 0 | // never return a null time progress due to being *before* the animation |
57 | 0 | // interval. However, it might be possible that we're behind on flushing |
58 | 0 | // causing us to get called *after* the animation interval. So, just in |
59 | 0 | // case, we override the fill mode to 'both' to ensure the progress |
60 | 0 | // is never null. |
61 | 0 | TimingParams timingToUse = SpecifiedTiming(); |
62 | 0 | timingToUse.SetFill(dom::FillMode::Both); |
63 | 0 | ComputedTiming computedTiming = GetComputedTiming(&timingToUse); |
64 | 0 |
|
65 | 0 | MOZ_ASSERT(!computedTiming.mProgress.IsNull(), |
66 | 0 | "Got a null progress for a fill mode of 'both'"); |
67 | 0 | MOZ_ASSERT(mKeyframes.Length() == 2, |
68 | 0 | "Should have two animation keyframes for a transition"); |
69 | 0 | return ComputedTimingFunction::GetPortion(mKeyframes[0].mTimingFunction, |
70 | 0 | computedTiming.mProgress.Value(), |
71 | 0 | computedTiming.mBeforeFlag); |
72 | 0 | } |
73 | | |
74 | | void |
75 | | ElementPropertyTransition::UpdateStartValueFromReplacedTransition() |
76 | 0 | { |
77 | 0 | if (!mReplacedTransition) { |
78 | 0 | return; |
79 | 0 | } |
80 | 0 | MOZ_ASSERT(nsCSSProps::PropHasFlags(TransitionProperty(), |
81 | 0 | CSSPropFlags::CanAnimateOnCompositor), |
82 | 0 | "The transition property should be able to be run on the " |
83 | 0 | "compositor"); |
84 | 0 | MOZ_ASSERT(mTarget && mTarget->mElement->OwnerDoc(), |
85 | 0 | "We should have a valid document at this moment"); |
86 | 0 |
|
87 | 0 | dom::DocumentTimeline* timeline = mTarget->mElement->OwnerDoc()->Timeline(); |
88 | 0 | ComputedTiming computedTiming = GetComputedTimingAt( |
89 | 0 | dom::CSSTransition::GetCurrentTimeAt(*timeline, |
90 | 0 | TimeStamp::Now(), |
91 | 0 | mReplacedTransition->mStartTime, |
92 | 0 | mReplacedTransition->mPlaybackRate), |
93 | 0 | mReplacedTransition->mTiming, |
94 | 0 | mReplacedTransition->mPlaybackRate); |
95 | 0 |
|
96 | 0 | if (!computedTiming.mProgress.IsNull()) { |
97 | 0 | double valuePosition = |
98 | 0 | ComputedTimingFunction::GetPortion(mReplacedTransition->mTimingFunction, |
99 | 0 | computedTiming.mProgress.Value(), |
100 | 0 | computedTiming.mBeforeFlag); |
101 | 0 |
|
102 | 0 | MOZ_ASSERT(mProperties.Length() == 1 && |
103 | 0 | mProperties[0].mSegments.Length() == 1, |
104 | 0 | "The transition should have one property and one segment"); |
105 | 0 | MOZ_ASSERT(mKeyframes.Length() == 2, |
106 | 0 | "Transitions should have exactly two animation keyframes"); |
107 | 0 | MOZ_ASSERT(mKeyframes[0].mPropertyValues.Length() == 1, |
108 | 0 | "Transitions should have exactly one property in their first " |
109 | 0 | "frame"); |
110 | 0 |
|
111 | 0 | const AnimationValue& replacedFrom = mReplacedTransition->mFromValue; |
112 | 0 | const AnimationValue& replacedTo = mReplacedTransition->mToValue; |
113 | 0 | AnimationValue startValue; |
114 | 0 | startValue.mServo = |
115 | 0 | Servo_AnimationValues_Interpolate(replacedFrom.mServo, |
116 | 0 | replacedTo.mServo, |
117 | 0 | valuePosition).Consume(); |
118 | 0 | if (startValue.mServo) { |
119 | 0 | mKeyframes[0].mPropertyValues[0].mServoDeclarationBlock = |
120 | 0 | Servo_AnimationValue_Uncompute(startValue.mServo).Consume(); |
121 | 0 | mProperties[0].mSegments[0].mFromValue = std::move(startValue); |
122 | 0 | } |
123 | 0 | } |
124 | 0 |
|
125 | 0 | mReplacedTransition.reset(); |
126 | 0 | } |
127 | | |
128 | | ////////////////////////// CSSTransition //////////////////////////// |
129 | | |
130 | | JSObject* |
131 | | CSSTransition::WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) |
132 | 0 | { |
133 | 0 | return dom::CSSTransition_Binding::Wrap(aCx, this, aGivenProto); |
134 | 0 | } |
135 | | |
136 | | void |
137 | | CSSTransition::GetTransitionProperty(nsString& aRetVal) const |
138 | 0 | { |
139 | 0 | MOZ_ASSERT(eCSSProperty_UNKNOWN != mTransitionProperty, |
140 | 0 | "Transition Property should be initialized"); |
141 | 0 | aRetVal = |
142 | 0 | NS_ConvertUTF8toUTF16(nsCSSProps::GetStringValue(mTransitionProperty)); |
143 | 0 | } |
144 | | |
145 | | AnimationPlayState |
146 | | CSSTransition::PlayStateFromJS() const |
147 | 0 | { |
148 | 0 | FlushUnanimatedStyle(); |
149 | 0 | return Animation::PlayStateFromJS(); |
150 | 0 | } |
151 | | |
152 | | bool |
153 | | CSSTransition::PendingFromJS() const |
154 | 0 | { |
155 | 0 | // Transitions don't become pending again after they start running but, if |
156 | 0 | // while the transition is still pending, style is updated in such a way |
157 | 0 | // that the transition will be canceled, we need to report false here. |
158 | 0 | // Hence we need to flush, but only when we're pending. |
159 | 0 | if (Pending()) { |
160 | 0 | FlushUnanimatedStyle(); |
161 | 0 | } |
162 | 0 | return Animation::PendingFromJS(); |
163 | 0 | } |
164 | | |
165 | | void |
166 | | CSSTransition::PlayFromJS(ErrorResult& aRv) |
167 | 0 | { |
168 | 0 | FlushUnanimatedStyle(); |
169 | 0 | Animation::PlayFromJS(aRv); |
170 | 0 | } |
171 | | |
172 | | void |
173 | | CSSTransition::UpdateTiming(SeekFlag aSeekFlag, SyncNotifyFlag aSyncNotifyFlag) |
174 | 0 | { |
175 | 0 | if (mNeedsNewAnimationIndexWhenRun && |
176 | 0 | PlayState() != AnimationPlayState::Idle) { |
177 | 0 | mAnimationIndex = sNextAnimationIndex++; |
178 | 0 | mNeedsNewAnimationIndexWhenRun = false; |
179 | 0 | } |
180 | 0 |
|
181 | 0 | Animation::UpdateTiming(aSeekFlag, aSyncNotifyFlag); |
182 | 0 | } |
183 | | |
184 | | void |
185 | | CSSTransition::QueueEvents(const StickyTimeDuration& aActiveTime) |
186 | 0 | { |
187 | 0 | if (!mOwningElement.IsSet()) { |
188 | 0 | return; |
189 | 0 | } |
190 | 0 | |
191 | 0 | nsPresContext* presContext = mOwningElement.GetPresContext(); |
192 | 0 | if (!presContext) { |
193 | 0 | return; |
194 | 0 | } |
195 | 0 | |
196 | 0 | static constexpr StickyTimeDuration zeroDuration = StickyTimeDuration(); |
197 | 0 |
|
198 | 0 | TransitionPhase currentPhase; |
199 | 0 | StickyTimeDuration intervalStartTime; |
200 | 0 | StickyTimeDuration intervalEndTime; |
201 | 0 |
|
202 | 0 | if (!mEffect) { |
203 | 0 | currentPhase = GetAnimationPhaseWithoutEffect<TransitionPhase>(*this); |
204 | 0 | } else { |
205 | 0 | ComputedTiming computedTiming = mEffect->GetComputedTiming(); |
206 | 0 |
|
207 | 0 | currentPhase = static_cast<TransitionPhase>(computedTiming.mPhase); |
208 | 0 | intervalStartTime = IntervalStartTime(computedTiming.mActiveDuration); |
209 | 0 | intervalEndTime = IntervalEndTime(computedTiming.mActiveDuration); |
210 | 0 | } |
211 | 0 |
|
212 | 0 | if (mPendingState != PendingState::NotPending && |
213 | 0 | (mPreviousTransitionPhase == TransitionPhase::Idle || |
214 | 0 | mPreviousTransitionPhase == TransitionPhase::Pending)) { |
215 | 0 | currentPhase = TransitionPhase::Pending; |
216 | 0 | } |
217 | 0 |
|
218 | 0 | if (currentPhase == mPreviousTransitionPhase) { |
219 | 0 | return; |
220 | 0 | } |
221 | 0 | |
222 | 0 | // TimeStamps to use for ordering the events when they are dispatched. We |
223 | 0 | // use a TimeStamp so we can compare events produced by different elements, |
224 | 0 | // perhaps even with different timelines. |
225 | 0 | // The zero timestamp is for transitionrun events where we ignore the delay |
226 | 0 | // for the purpose of ordering events. |
227 | 0 | TimeStamp zeroTimeStamp = AnimationTimeToTimeStamp(zeroDuration); |
228 | 0 | TimeStamp startTimeStamp = ElapsedTimeToTimeStamp(intervalStartTime); |
229 | 0 | TimeStamp endTimeStamp = ElapsedTimeToTimeStamp(intervalEndTime); |
230 | 0 |
|
231 | 0 | AutoTArray<AnimationEventInfo, 3> events; |
232 | 0 |
|
233 | 0 | auto appendTransitionEvent = [&](EventMessage aMessage, |
234 | 0 | const StickyTimeDuration& aElapsedTime, |
235 | 0 | const TimeStamp& aScheduledEventTimeStamp) { |
236 | 0 | double elapsedTime = aElapsedTime.ToSeconds(); |
237 | 0 | if (aMessage == eTransitionCancel) { |
238 | 0 | // 0 is an inappropriate value for this callsite. What we need to do is |
239 | 0 | // use a single random value for all increasing times reportable. |
240 | 0 | // That is to say, whenever elapsedTime goes negative (because an |
241 | 0 | // animation restarts, something rewinds the animation, or otherwise) |
242 | 0 | // a new random value for the mix-in must be generated. |
243 | 0 | elapsedTime = nsRFPService::ReduceTimePrecisionAsSecs(elapsedTime, 0, TimerPrecisionType::RFPOnly); |
244 | 0 | } |
245 | 0 | events.AppendElement(AnimationEventInfo(TransitionProperty(), |
246 | 0 | mOwningElement.Target(), |
247 | 0 | aMessage, |
248 | 0 | elapsedTime, |
249 | 0 | aScheduledEventTimeStamp, |
250 | 0 | this)); |
251 | 0 | }; |
252 | 0 |
|
253 | 0 | // Handle cancel events first |
254 | 0 | if ((mPreviousTransitionPhase != TransitionPhase::Idle && |
255 | 0 | mPreviousTransitionPhase != TransitionPhase::After) && |
256 | 0 | currentPhase == TransitionPhase::Idle) { |
257 | 0 | appendTransitionEvent(eTransitionCancel, |
258 | 0 | aActiveTime, |
259 | 0 | GetTimelineCurrentTimeAsTimeStamp()); |
260 | 0 | } |
261 | 0 |
|
262 | 0 | // All other events |
263 | 0 | switch (mPreviousTransitionPhase) { |
264 | 0 | case TransitionPhase::Idle: |
265 | 0 | if (currentPhase == TransitionPhase::Pending || |
266 | 0 | currentPhase == TransitionPhase::Before) { |
267 | 0 | appendTransitionEvent(eTransitionRun, intervalStartTime, zeroTimeStamp); |
268 | 0 | } else if (currentPhase == TransitionPhase::Active) { |
269 | 0 | appendTransitionEvent(eTransitionRun, intervalStartTime, zeroTimeStamp); |
270 | 0 | appendTransitionEvent(eTransitionStart, |
271 | 0 | intervalStartTime, |
272 | 0 | startTimeStamp); |
273 | 0 | } else if (currentPhase == TransitionPhase::After) { |
274 | 0 | appendTransitionEvent(eTransitionRun, intervalStartTime, zeroTimeStamp); |
275 | 0 | appendTransitionEvent(eTransitionStart, |
276 | 0 | intervalStartTime, |
277 | 0 | startTimeStamp); |
278 | 0 | appendTransitionEvent(eTransitionEnd, intervalEndTime, endTimeStamp); |
279 | 0 | } |
280 | 0 | break; |
281 | 0 |
|
282 | 0 | case TransitionPhase::Pending: |
283 | 0 | case TransitionPhase::Before: |
284 | 0 | if (currentPhase == TransitionPhase::Active) { |
285 | 0 | appendTransitionEvent(eTransitionStart, |
286 | 0 | intervalStartTime, |
287 | 0 | startTimeStamp); |
288 | 0 | } else if (currentPhase == TransitionPhase::After) { |
289 | 0 | appendTransitionEvent(eTransitionStart, |
290 | 0 | intervalStartTime, |
291 | 0 | startTimeStamp); |
292 | 0 | appendTransitionEvent(eTransitionEnd, intervalEndTime, endTimeStamp); |
293 | 0 | } |
294 | 0 | break; |
295 | 0 |
|
296 | 0 | case TransitionPhase::Active: |
297 | 0 | if (currentPhase == TransitionPhase::After) { |
298 | 0 | appendTransitionEvent(eTransitionEnd, intervalEndTime, endTimeStamp); |
299 | 0 | } else if (currentPhase == TransitionPhase::Before) { |
300 | 0 | appendTransitionEvent(eTransitionEnd, |
301 | 0 | intervalStartTime, |
302 | 0 | startTimeStamp); |
303 | 0 | } |
304 | 0 | break; |
305 | 0 |
|
306 | 0 | case TransitionPhase::After: |
307 | 0 | if (currentPhase == TransitionPhase::Active) { |
308 | 0 | appendTransitionEvent(eTransitionStart, |
309 | 0 | intervalEndTime, |
310 | 0 | startTimeStamp); |
311 | 0 | } else if (currentPhase == TransitionPhase::Before) { |
312 | 0 | appendTransitionEvent(eTransitionStart, |
313 | 0 | intervalEndTime, |
314 | 0 | startTimeStamp); |
315 | 0 | appendTransitionEvent(eTransitionEnd, |
316 | 0 | intervalStartTime, |
317 | 0 | endTimeStamp); |
318 | 0 | } |
319 | 0 | break; |
320 | 0 | } |
321 | 0 | mPreviousTransitionPhase = currentPhase; |
322 | 0 |
|
323 | 0 | if (!events.IsEmpty()) { |
324 | 0 | presContext->AnimationEventDispatcher()->QueueEvents(std::move(events)); |
325 | 0 | } |
326 | 0 | } |
327 | | |
328 | | void |
329 | | CSSTransition::Tick() |
330 | 0 | { |
331 | 0 | Animation::Tick(); |
332 | 0 | QueueEvents(); |
333 | 0 | } |
334 | | |
335 | | nsCSSPropertyID |
336 | | CSSTransition::TransitionProperty() const |
337 | 0 | { |
338 | 0 | MOZ_ASSERT(eCSSProperty_UNKNOWN != mTransitionProperty, |
339 | 0 | "Transition property should be initialized"); |
340 | 0 | return mTransitionProperty; |
341 | 0 | } |
342 | | |
343 | | AnimationValue |
344 | | CSSTransition::ToValue() const |
345 | 0 | { |
346 | 0 | MOZ_ASSERT(!mTransitionToValue.IsNull(), |
347 | 0 | "Transition ToValue should be initialized"); |
348 | 0 | return mTransitionToValue; |
349 | 0 | } |
350 | | |
351 | | bool |
352 | | CSSTransition::HasLowerCompositeOrderThan(const CSSTransition& aOther) const |
353 | 0 | { |
354 | 0 | MOZ_ASSERT(IsTiedToMarkup() && aOther.IsTiedToMarkup(), |
355 | 0 | "Should only be called for CSS transitions that are sorted " |
356 | 0 | "as CSS transitions (i.e. tied to CSS markup)"); |
357 | 0 |
|
358 | 0 | // 0. Object-equality case |
359 | 0 | if (&aOther == this) { |
360 | 0 | return false; |
361 | 0 | } |
362 | 0 | |
363 | 0 | // 1. Sort by document order |
364 | 0 | if (!mOwningElement.Equals(aOther.mOwningElement)) { |
365 | 0 | return mOwningElement.LessThan( |
366 | 0 | const_cast<CSSTransition*>(this)->CachedChildIndexRef(), |
367 | 0 | aOther.mOwningElement, |
368 | 0 | const_cast<CSSTransition*>(&aOther)->CachedChildIndexRef()); |
369 | 0 | } |
370 | 0 | |
371 | 0 | // 2. (Same element and pseudo): Sort by transition generation |
372 | 0 | if (mAnimationIndex != aOther.mAnimationIndex) { |
373 | 0 | return mAnimationIndex < aOther.mAnimationIndex; |
374 | 0 | } |
375 | 0 | |
376 | 0 | // 3. (Same transition generation): Sort by transition property |
377 | 0 | return nsCSSProps::GetStringValue(TransitionProperty()) < |
378 | 0 | nsCSSProps::GetStringValue(aOther.TransitionProperty()); |
379 | 0 | } |
380 | | |
381 | | /* static */ Nullable<TimeDuration> |
382 | | CSSTransition::GetCurrentTimeAt(const dom::DocumentTimeline& aTimeline, |
383 | | const TimeStamp& aBaseTime, |
384 | | const TimeDuration& aStartTime, |
385 | | double aPlaybackRate) |
386 | 0 | { |
387 | 0 | Nullable<TimeDuration> result; |
388 | 0 |
|
389 | 0 | Nullable<TimeDuration> timelineTime = aTimeline.ToTimelineTime(aBaseTime); |
390 | 0 | if (!timelineTime.IsNull()) { |
391 | 0 | result.SetValue((timelineTime.Value() - aStartTime) |
392 | 0 | .MultDouble(aPlaybackRate)); |
393 | 0 | } |
394 | 0 |
|
395 | 0 | return result; |
396 | 0 | } |
397 | | |
398 | | void |
399 | | CSSTransition::SetEffectFromStyle(dom::AnimationEffect* aEffect) |
400 | 0 | { |
401 | 0 | Animation::SetEffectNoUpdate(aEffect); |
402 | 0 |
|
403 | 0 | // Initialize transition property. |
404 | 0 | ElementPropertyTransition* pt = aEffect ? aEffect->AsTransition() : nullptr; |
405 | 0 | if (eCSSProperty_UNKNOWN == mTransitionProperty && pt) { |
406 | 0 | mTransitionProperty = pt->TransitionProperty(); |
407 | 0 | mTransitionToValue = pt->ToValue(); |
408 | 0 | } |
409 | 0 | } |
410 | | |
411 | | ////////////////////////// nsTransitionManager //////////////////////////// |
412 | | |
413 | | |
414 | | static inline bool |
415 | | ExtractNonDiscreteComputedValue(nsCSSPropertyID aProperty, |
416 | | const ComputedStyle& aComputedStyle, |
417 | | AnimationValue& aAnimationValue) |
418 | 0 | { |
419 | 0 | if (Servo_Property_IsDiscreteAnimatable(aProperty) && |
420 | 0 | aProperty != eCSSProperty_visibility) { |
421 | 0 | return false; |
422 | 0 | } |
423 | 0 | |
424 | 0 | aAnimationValue.mServo = |
425 | 0 | Servo_ComputedValues_ExtractAnimationValue(&aComputedStyle, |
426 | 0 | aProperty).Consume(); |
427 | 0 | return !!aAnimationValue.mServo; |
428 | 0 | } |
429 | | |
430 | | |
431 | | bool |
432 | | nsTransitionManager::UpdateTransitions( |
433 | | dom::Element *aElement, |
434 | | CSSPseudoElementType aPseudoType, |
435 | | const ComputedStyle& aOldStyle, |
436 | | const ComputedStyle& aNewStyle) |
437 | 0 | { |
438 | 0 | if (!mPresContext->IsDynamic()) { |
439 | 0 | // For print or print preview, ignore transitions. |
440 | 0 | return false; |
441 | 0 | } |
442 | 0 | |
443 | 0 | CSSTransitionCollection* collection = |
444 | 0 | CSSTransitionCollection::GetAnimationCollection(aElement, aPseudoType); |
445 | 0 | const nsStyleDisplay* disp = |
446 | 0 | aNewStyle.ComputedData()->GetStyleDisplay(); |
447 | 0 | return DoUpdateTransitions(*disp, |
448 | 0 | aElement, aPseudoType, |
449 | 0 | collection, |
450 | 0 | aOldStyle, aNewStyle); |
451 | 0 | } |
452 | | |
453 | | bool |
454 | | nsTransitionManager::DoUpdateTransitions( |
455 | | const nsStyleDisplay& aDisp, |
456 | | dom::Element* aElement, |
457 | | CSSPseudoElementType aPseudoType, |
458 | | CSSTransitionCollection*& aElementTransitions, |
459 | | const ComputedStyle& aOldStyle, |
460 | | const ComputedStyle& aNewStyle) |
461 | 0 | { |
462 | 0 | MOZ_ASSERT(!aElementTransitions || |
463 | 0 | aElementTransitions->mElement == aElement, "Element mismatch"); |
464 | 0 |
|
465 | 0 | // Per http://lists.w3.org/Archives/Public/www-style/2009Aug/0109.html |
466 | 0 | // I'll consider only the transitions from the number of items in |
467 | 0 | // 'transition-property' on down, and later ones will override earlier |
468 | 0 | // ones (tracked using |propertiesChecked|). |
469 | 0 | bool startedAny = false; |
470 | 0 | nsCSSPropertyIDSet propertiesChecked; |
471 | 0 | for (uint32_t i = aDisp.mTransitionPropertyCount; i--; ) { |
472 | 0 | // We're not going to look at any further transitions, so we can just avoid |
473 | 0 | // looking at this if we know it will not start any transitions. |
474 | 0 | if (i == 0 && aDisp.GetTransitionCombinedDuration(i) <= 0.0f) { |
475 | 0 | continue; |
476 | 0 | } |
477 | 0 | |
478 | 0 | nsCSSPropertyID property = aDisp.GetTransitionProperty(i); |
479 | 0 | if (property == eCSSPropertyExtra_no_properties || |
480 | 0 | property == eCSSPropertyExtra_variable || |
481 | 0 | property == eCSSProperty_UNKNOWN) { |
482 | 0 | // Nothing to do. |
483 | 0 | continue; |
484 | 0 | } |
485 | 0 | // We might have something to transition. See if any of the |
486 | 0 | // properties in question changed and are animatable. |
487 | 0 | // FIXME: Would be good to find a way to share code between this |
488 | 0 | // interpretation of transition-property and the one below. |
489 | 0 | // FIXME(emilio): This should probably just use the "all" shorthand id, and |
490 | 0 | // we should probably remove eCSSPropertyExtra_all_properties. |
491 | 0 | if (property == eCSSPropertyExtra_all_properties) { |
492 | 0 | for (nsCSSPropertyID p = nsCSSPropertyID(0); |
493 | 0 | p < eCSSProperty_COUNT_no_shorthands; |
494 | 0 | p = nsCSSPropertyID(p + 1)) { |
495 | 0 | if (!nsCSSProps::IsEnabled(p, CSSEnabledState::eForAllContent)) { |
496 | 0 | continue; |
497 | 0 | } |
498 | 0 | startedAny |= |
499 | 0 | ConsiderInitiatingTransition(p, aDisp, i, aElement, aPseudoType, |
500 | 0 | aElementTransitions, |
501 | 0 | aOldStyle, aNewStyle, |
502 | 0 | propertiesChecked); |
503 | 0 | } |
504 | 0 | } else if (nsCSSProps::IsShorthand(property)) { |
505 | 0 | CSSPROPS_FOR_SHORTHAND_SUBPROPERTIES(subprop, property, |
506 | 0 | CSSEnabledState::eForAllContent) |
507 | 0 | { |
508 | 0 | startedAny |= |
509 | 0 | ConsiderInitiatingTransition(*subprop, aDisp, i, aElement, aPseudoType, |
510 | 0 | aElementTransitions, |
511 | 0 | aOldStyle, aNewStyle, |
512 | 0 | propertiesChecked); |
513 | 0 | } |
514 | 0 | } else { |
515 | 0 | startedAny |= |
516 | 0 | ConsiderInitiatingTransition(property, aDisp, i, aElement, aPseudoType, |
517 | 0 | aElementTransitions, |
518 | 0 | aOldStyle, aNewStyle, |
519 | 0 | propertiesChecked); |
520 | 0 | } |
521 | 0 | } |
522 | 0 |
|
523 | 0 | // Stop any transitions for properties that are no longer in |
524 | 0 | // 'transition-property', including finished transitions. |
525 | 0 | // Also stop any transitions (and remove any finished transitions) |
526 | 0 | // for properties that just changed (and are still in the set of |
527 | 0 | // properties to transition), but for which we didn't just start the |
528 | 0 | // transition. This can happen delay and duration are both zero, or |
529 | 0 | // because the new value is not interpolable. |
530 | 0 | // Note that we also do the latter set of work in |
531 | 0 | // nsTransitionManager::PruneCompletedTransitions. |
532 | 0 | if (aElementTransitions) { |
533 | 0 | bool checkProperties = |
534 | 0 | aDisp.GetTransitionProperty(0) != eCSSPropertyExtra_all_properties; |
535 | 0 | nsCSSPropertyIDSet allTransitionProperties; |
536 | 0 | if (checkProperties) { |
537 | 0 | for (uint32_t i = aDisp.mTransitionPropertyCount; i-- != 0; ) { |
538 | 0 | // FIXME: Would be good to find a way to share code between this |
539 | 0 | // interpretation of transition-property and the one above. |
540 | 0 | nsCSSPropertyID property = aDisp.GetTransitionProperty(i); |
541 | 0 | if (property == eCSSPropertyExtra_no_properties || |
542 | 0 | property == eCSSPropertyExtra_variable || |
543 | 0 | property == eCSSProperty_UNKNOWN) { |
544 | 0 | // Nothing to do, but need to exclude this from cases below. |
545 | 0 | } else if (property == eCSSPropertyExtra_all_properties) { |
546 | 0 | for (nsCSSPropertyID p = nsCSSPropertyID(0); |
547 | 0 | p < eCSSProperty_COUNT_no_shorthands; |
548 | 0 | p = nsCSSPropertyID(p + 1)) { |
549 | 0 | allTransitionProperties.AddProperty(nsCSSProps::Physicalize(p, aNewStyle)); |
550 | 0 | } |
551 | 0 | } else if (nsCSSProps::IsShorthand(property)) { |
552 | 0 | CSSPROPS_FOR_SHORTHAND_SUBPROPERTIES( |
553 | 0 | subprop, property, CSSEnabledState::eForAllContent) { |
554 | 0 | auto p = nsCSSProps::Physicalize(*subprop, aNewStyle); |
555 | 0 | allTransitionProperties.AddProperty(p); |
556 | 0 | } |
557 | 0 | } else { |
558 | 0 | allTransitionProperties.AddProperty( |
559 | 0 | nsCSSProps::Physicalize(property, aNewStyle)); |
560 | 0 | } |
561 | 0 | } |
562 | 0 | } |
563 | 0 |
|
564 | 0 | OwningCSSTransitionPtrArray& animations = aElementTransitions->mAnimations; |
565 | 0 | size_t i = animations.Length(); |
566 | 0 | MOZ_ASSERT(i != 0, "empty transitions list?"); |
567 | 0 | AnimationValue currentValue; |
568 | 0 | do { |
569 | 0 | --i; |
570 | 0 | CSSTransition* anim = animations[i]; |
571 | 0 | // properties no longer in 'transition-property' |
572 | 0 | if ((checkProperties && |
573 | 0 | !allTransitionProperties.HasProperty(anim->TransitionProperty())) || |
574 | 0 | // properties whose computed values changed but for which we |
575 | 0 | // did not start a new transition (because delay and |
576 | 0 | // duration are both zero, or because the new value is not |
577 | 0 | // interpolable); a new transition would have anim->ToValue() |
578 | 0 | // matching currentValue |
579 | 0 | !ExtractNonDiscreteComputedValue(anim->TransitionProperty(), |
580 | 0 | aNewStyle, currentValue) || |
581 | 0 | currentValue != anim->ToValue()) { |
582 | 0 | // stop the transition |
583 | 0 | if (anim->HasCurrentEffect()) { |
584 | 0 | EffectSet* effectSet = |
585 | 0 | EffectSet::GetEffectSet(aElement, aPseudoType); |
586 | 0 | if (effectSet) { |
587 | 0 | effectSet->UpdateAnimationGeneration(mPresContext); |
588 | 0 | } |
589 | 0 | } |
590 | 0 | anim->CancelFromStyle(); |
591 | 0 | animations.RemoveElementAt(i); |
592 | 0 | } |
593 | 0 | } while (i != 0); |
594 | 0 |
|
595 | 0 | if (animations.IsEmpty()) { |
596 | 0 | aElementTransitions->Destroy(); |
597 | 0 | aElementTransitions = nullptr; |
598 | 0 | } |
599 | 0 | } |
600 | 0 |
|
601 | 0 | return startedAny; |
602 | 0 | } |
603 | | |
604 | | static Keyframe& |
605 | | AppendKeyframe(double aOffset, |
606 | | nsCSSPropertyID aProperty, |
607 | | AnimationValue&& aValue, |
608 | | nsTArray<Keyframe>& aKeyframes) |
609 | 0 | { |
610 | 0 | Keyframe& frame = *aKeyframes.AppendElement(); |
611 | 0 | frame.mOffset.emplace(aOffset); |
612 | 0 |
|
613 | 0 | if (aValue.mServo) { |
614 | 0 | RefPtr<RawServoDeclarationBlock> decl = |
615 | 0 | Servo_AnimationValue_Uncompute(aValue.mServo).Consume(); |
616 | 0 | frame.mPropertyValues.AppendElement( |
617 | 0 | PropertyValuePair(aProperty, std::move(decl))); |
618 | 0 | } else { |
619 | 0 | MOZ_CRASH("old style system disabled"); |
620 | 0 | } |
621 | 0 | return frame; |
622 | 0 | } |
623 | | |
624 | | static nsTArray<Keyframe> |
625 | | GetTransitionKeyframes(nsCSSPropertyID aProperty, |
626 | | AnimationValue&& aStartValue, |
627 | | AnimationValue&& aEndValue, |
628 | | const nsTimingFunction& aTimingFunction) |
629 | 0 | { |
630 | 0 | nsTArray<Keyframe> keyframes(2); |
631 | 0 |
|
632 | 0 | Keyframe& fromFrame = AppendKeyframe(0.0, aProperty, std::move(aStartValue), |
633 | 0 | keyframes); |
634 | 0 | if (aTimingFunction.mType != nsTimingFunction::Type::Linear) { |
635 | 0 | fromFrame.mTimingFunction.emplace(); |
636 | 0 | fromFrame.mTimingFunction->Init(aTimingFunction); |
637 | 0 | } |
638 | 0 |
|
639 | 0 | AppendKeyframe(1.0, aProperty, std::move(aEndValue), keyframes); |
640 | 0 |
|
641 | 0 | return keyframes; |
642 | 0 | } |
643 | | |
644 | | static bool |
645 | | IsTransitionable(nsCSSPropertyID aProperty) |
646 | 0 | { |
647 | 0 | return Servo_Property_IsTransitionable(aProperty); |
648 | 0 | } |
649 | | |
650 | | bool |
651 | | nsTransitionManager::ConsiderInitiatingTransition( |
652 | | nsCSSPropertyID aProperty, |
653 | | const nsStyleDisplay& aStyleDisplay, |
654 | | uint32_t transitionIdx, |
655 | | dom::Element* aElement, |
656 | | CSSPseudoElementType aPseudoType, |
657 | | CSSTransitionCollection*& aElementTransitions, |
658 | | const ComputedStyle& aOldStyle, |
659 | | const ComputedStyle& aNewStyle, |
660 | | nsCSSPropertyIDSet& aPropertiesChecked) |
661 | 0 | { |
662 | 0 | // IsShorthand itself will assert if aProperty is not a property. |
663 | 0 | MOZ_ASSERT(!nsCSSProps::IsShorthand(aProperty), "property out of range"); |
664 | 0 | NS_ASSERTION(!aElementTransitions || |
665 | 0 | aElementTransitions->mElement == aElement, "Element mismatch"); |
666 | 0 |
|
667 | 0 | aProperty = nsCSSProps::Physicalize(aProperty, aNewStyle); |
668 | 0 |
|
669 | 0 | // A later item in transition-property already specified a transition for |
670 | 0 | // this property, so we ignore this one. |
671 | 0 | // |
672 | 0 | // See http://lists.w3.org/Archives/Public/www-style/2009Aug/0109.html . |
673 | 0 | if (aPropertiesChecked.HasProperty(aProperty)) { |
674 | 0 | return false; |
675 | 0 | } |
676 | 0 | |
677 | 0 | aPropertiesChecked.AddProperty(aProperty); |
678 | 0 |
|
679 | 0 | if (!IsTransitionable(aProperty)) { |
680 | 0 | return false; |
681 | 0 | } |
682 | 0 | |
683 | 0 | float delay = aStyleDisplay.GetTransitionDelay(transitionIdx); |
684 | 0 |
|
685 | 0 | // The spec says a negative duration is treated as zero. |
686 | 0 | float duration = |
687 | 0 | std::max(aStyleDisplay.GetTransitionDuration(transitionIdx), 0.0f); |
688 | 0 |
|
689 | 0 | // If the combined duration of this transition is 0 or less don't start a |
690 | 0 | // transition. |
691 | 0 | if (delay + duration <= 0.0f) { |
692 | 0 | return false; |
693 | 0 | } |
694 | 0 | |
695 | 0 | dom::DocumentTimeline* timeline = aElement->OwnerDoc()->Timeline(); |
696 | 0 |
|
697 | 0 | AnimationValue startValue, endValue; |
698 | 0 | bool haveValues = |
699 | 0 | ExtractNonDiscreteComputedValue(aProperty, aOldStyle, startValue) && |
700 | 0 | ExtractNonDiscreteComputedValue(aProperty, aNewStyle, endValue); |
701 | 0 |
|
702 | 0 | bool haveChange = startValue != endValue; |
703 | 0 |
|
704 | 0 | bool shouldAnimate = |
705 | 0 | haveValues && |
706 | 0 | haveChange && |
707 | 0 | startValue.IsInterpolableWith(aProperty, endValue); |
708 | 0 |
|
709 | 0 | bool haveCurrentTransition = false; |
710 | 0 | size_t currentIndex = nsTArray<ElementPropertyTransition>::NoIndex; |
711 | 0 | const ElementPropertyTransition *oldPT = nullptr; |
712 | 0 | if (aElementTransitions) { |
713 | 0 | OwningCSSTransitionPtrArray& animations = aElementTransitions->mAnimations; |
714 | 0 | for (size_t i = 0, i_end = animations.Length(); i < i_end; ++i) { |
715 | 0 | if (animations[i]->TransitionProperty() == aProperty) { |
716 | 0 | haveCurrentTransition = true; |
717 | 0 | currentIndex = i; |
718 | 0 | oldPT = animations[i]->GetEffect() |
719 | 0 | ? animations[i]->GetEffect()->AsTransition() |
720 | 0 | : nullptr; |
721 | 0 | break; |
722 | 0 | } |
723 | 0 | } |
724 | 0 | } |
725 | 0 |
|
726 | 0 | // If we got a style change that changed the value to the endpoint |
727 | 0 | // of the currently running transition, we don't want to interrupt |
728 | 0 | // its timing function. |
729 | 0 | // This needs to be before the !shouldAnimate && haveCurrentTransition |
730 | 0 | // case below because we might be close enough to the end of the |
731 | 0 | // transition that the current value rounds to the final value. In |
732 | 0 | // this case, we'll end up with shouldAnimate as false (because |
733 | 0 | // there's no value change), but we need to return early here rather |
734 | 0 | // than cancel the running transition because shouldAnimate is false! |
735 | 0 | // |
736 | 0 | // Likewise, if we got a style change that changed the value to the |
737 | 0 | // endpoint of our finished transition, we also don't want to start |
738 | 0 | // a new transition for the reasons described in |
739 | 0 | // https://lists.w3.org/Archives/Public/www-style/2015Jan/0444.html . |
740 | 0 | if (haveCurrentTransition && haveValues && |
741 | 0 | aElementTransitions->mAnimations[currentIndex]->ToValue() == endValue) { |
742 | 0 | // GetAnimationRule already called RestyleForAnimation. |
743 | 0 | return false; |
744 | 0 | } |
745 | 0 | |
746 | 0 | if (!shouldAnimate) { |
747 | 0 | if (haveCurrentTransition) { |
748 | 0 | // We're in the middle of a transition, and just got a non-transition |
749 | 0 | // style change to something that we can't animate. This might happen |
750 | 0 | // because we got a non-transition style change changing to the current |
751 | 0 | // in-progress value (which is particularly easy to cause when we're |
752 | 0 | // currently in the 'transition-delay'). It also might happen because we |
753 | 0 | // just got a style change to a value that can't be interpolated. |
754 | 0 | OwningCSSTransitionPtrArray& animations = |
755 | 0 | aElementTransitions->mAnimations; |
756 | 0 | animations[currentIndex]->CancelFromStyle(); |
757 | 0 | oldPT = nullptr; // Clear pointer so it doesn't dangle |
758 | 0 | animations.RemoveElementAt(currentIndex); |
759 | 0 | EffectSet* effectSet = EffectSet::GetEffectSet(aElement, aPseudoType); |
760 | 0 | if (effectSet) { |
761 | 0 | effectSet->UpdateAnimationGeneration(mPresContext); |
762 | 0 | } |
763 | 0 |
|
764 | 0 | if (animations.IsEmpty()) { |
765 | 0 | aElementTransitions->Destroy(); |
766 | 0 | // |aElementTransitions| is now a dangling pointer! |
767 | 0 | aElementTransitions = nullptr; |
768 | 0 | } |
769 | 0 | // GetAnimationRule already called RestyleForAnimation. |
770 | 0 | } |
771 | 0 | return false; |
772 | 0 | } |
773 | 0 |
|
774 | 0 | const nsTimingFunction &tf = |
775 | 0 | aStyleDisplay.GetTransitionTimingFunction(transitionIdx); |
776 | 0 |
|
777 | 0 | AnimationValue startForReversingTest = startValue; |
778 | 0 | double reversePortion = 1.0; |
779 | 0 |
|
780 | 0 | // If the new transition reverses an existing one, we'll need to |
781 | 0 | // handle the timing differently. |
782 | 0 | // FIXME: Move mStartForReversingTest, mReversePortion to CSSTransition, |
783 | 0 | // and set the timing function on transitions as an effect-level |
784 | 0 | // easing (rather than keyframe-level easing). (Bug 1292001) |
785 | 0 | if (haveCurrentTransition && |
786 | 0 | aElementTransitions->mAnimations[currentIndex]->HasCurrentEffect() && |
787 | 0 | oldPT && |
788 | 0 | oldPT->mStartForReversingTest == endValue) { |
789 | 0 | // Compute the appropriate negative transition-delay such that right |
790 | 0 | // now we'd end up at the current position. |
791 | 0 | double valuePortion = |
792 | 0 | oldPT->CurrentValuePortion() * oldPT->mReversePortion + |
793 | 0 | (1.0 - oldPT->mReversePortion); |
794 | 0 | // A timing function with negative y1 (or y2!) might make |
795 | 0 | // valuePortion negative. In this case, we still want to apply our |
796 | 0 | // reversing logic based on relative distances, not make duration |
797 | 0 | // negative. |
798 | 0 | if (valuePortion < 0.0) { |
799 | 0 | valuePortion = -valuePortion; |
800 | 0 | } |
801 | 0 | // A timing function with y2 (or y1!) greater than one might |
802 | 0 | // advance past its terminal value. It's probably a good idea to |
803 | 0 | // clamp valuePortion to be at most one to preserve the invariant |
804 | 0 | // that a transition will complete within at most its specified |
805 | 0 | // time. |
806 | 0 | if (valuePortion > 1.0) { |
807 | 0 | valuePortion = 1.0; |
808 | 0 | } |
809 | 0 |
|
810 | 0 | // Negative delays are essentially part of the transition |
811 | 0 | // function, so reduce them along with the duration, but don't |
812 | 0 | // reduce positive delays. |
813 | 0 | if (delay < 0.0f) { |
814 | 0 | delay *= valuePortion; |
815 | 0 | } |
816 | 0 |
|
817 | 0 | duration *= valuePortion; |
818 | 0 |
|
819 | 0 | startForReversingTest = oldPT->ToValue(); |
820 | 0 | reversePortion = valuePortion; |
821 | 0 | } |
822 | 0 |
|
823 | 0 | TimingParams timing = |
824 | 0 | TimingParamsFromCSSParams(duration, delay, |
825 | 0 | 1.0 /* iteration count */, |
826 | 0 | dom::PlaybackDirection::Normal, |
827 | 0 | dom::FillMode::Backwards); |
828 | 0 |
|
829 | 0 | // aElement is non-null here, so we emplace it directly. |
830 | 0 | Maybe<OwningAnimationTarget> target; |
831 | 0 | target.emplace(aElement, aPseudoType); |
832 | 0 | KeyframeEffectParams effectOptions; |
833 | 0 | RefPtr<ElementPropertyTransition> pt = |
834 | 0 | new ElementPropertyTransition(aElement->OwnerDoc(), target, timing, |
835 | 0 | startForReversingTest, reversePortion, |
836 | 0 | effectOptions); |
837 | 0 |
|
838 | 0 | pt->SetKeyframes(GetTransitionKeyframes(aProperty, |
839 | 0 | std::move(startValue), std::move(endValue), tf), |
840 | 0 | &aNewStyle); |
841 | 0 |
|
842 | 0 | RefPtr<CSSTransition> animation = |
843 | 0 | new CSSTransition(mPresContext->Document()->GetScopeObject()); |
844 | 0 | animation->SetOwningElement(OwningElementRef(*aElement, aPseudoType)); |
845 | 0 | animation->SetTimelineNoUpdate(timeline); |
846 | 0 | animation->SetCreationSequence( |
847 | 0 | mPresContext->RestyleManager()->GetAnimationGeneration()); |
848 | 0 | animation->SetEffectFromStyle(pt); |
849 | 0 | animation->PlayFromStyle(); |
850 | 0 |
|
851 | 0 | if (!aElementTransitions) { |
852 | 0 | bool createdCollection = false; |
853 | 0 | aElementTransitions = |
854 | 0 | CSSTransitionCollection::GetOrCreateAnimationCollection( |
855 | 0 | aElement, aPseudoType, &createdCollection); |
856 | 0 | if (!aElementTransitions) { |
857 | 0 | MOZ_ASSERT(!createdCollection, "outparam should agree with return value"); |
858 | 0 | NS_WARNING("allocating collection failed"); |
859 | 0 | return false; |
860 | 0 | } |
861 | 0 |
|
862 | 0 | if (createdCollection) { |
863 | 0 | AddElementCollection(aElementTransitions); |
864 | 0 | } |
865 | 0 | } |
866 | 0 |
|
867 | 0 | OwningCSSTransitionPtrArray& animations = aElementTransitions->mAnimations; |
868 | | #ifdef DEBUG |
869 | | for (size_t i = 0, i_end = animations.Length(); i < i_end; ++i) { |
870 | | MOZ_ASSERT( |
871 | | i == currentIndex || animations[i]->TransitionProperty() != aProperty, |
872 | | "duplicate transitions for property"); |
873 | | } |
874 | | #endif |
875 | 0 | if (haveCurrentTransition) { |
876 | 0 | // If this new transition is replacing an existing transition that is running |
877 | 0 | // on the compositor, we store select parameters from the replaced transition |
878 | 0 | // so that later, once all scripts have run, we can update the start value |
879 | 0 | // of the transition using TimeStamp::Now(). This allows us to avoid a |
880 | 0 | // large jump when starting a new transition when the main thread lags behind |
881 | 0 | // the compositor. |
882 | 0 | if (oldPT && |
883 | 0 | oldPT->IsCurrent() && |
884 | 0 | oldPT->IsRunningOnCompositor() && |
885 | 0 | !oldPT->GetAnimation()->GetStartTime().IsNull() && |
886 | 0 | timeline == oldPT->GetAnimation()->GetTimeline()) { |
887 | 0 | const AnimationPropertySegment& segment = |
888 | 0 | oldPT->Properties()[0].mSegments[0]; |
889 | 0 | pt->mReplacedTransition.emplace( |
890 | 0 | ElementPropertyTransition::ReplacedTransitionProperties({ |
891 | 0 | oldPT->GetAnimation()->GetStartTime().Value(), |
892 | 0 | oldPT->GetAnimation()->PlaybackRate(), |
893 | 0 | oldPT->SpecifiedTiming(), |
894 | 0 | segment.mTimingFunction, |
895 | 0 | segment.mFromValue, |
896 | 0 | segment.mToValue |
897 | 0 | }) |
898 | 0 | ); |
899 | 0 | } |
900 | 0 | animations[currentIndex]->CancelFromStyle(); |
901 | 0 | oldPT = nullptr; // Clear pointer so it doesn't dangle |
902 | 0 | animations[currentIndex] = animation; |
903 | 0 | } else { |
904 | 0 | if (!animations.AppendElement(animation)) { |
905 | 0 | NS_WARNING("out of memory"); |
906 | 0 | return false; |
907 | 0 | } |
908 | 0 | } |
909 | 0 |
|
910 | 0 | EffectSet* effectSet = EffectSet::GetEffectSet(aElement, aPseudoType); |
911 | 0 | if (effectSet) { |
912 | 0 | effectSet->UpdateAnimationGeneration(mPresContext); |
913 | 0 | } |
914 | 0 |
|
915 | 0 | return true; |
916 | 0 | } |
917 | | |