tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

nsTransitionManager.cpp (21826B)


      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 
     11 #include "AnimatedPropertyIDSet.h"
     12 #include "CSSPropertyId.h"
     13 #include "mozilla/ComputedStyle.h"
     14 #include "mozilla/EffectSet.h"
     15 #include "mozilla/ElementAnimationData.h"
     16 #include "mozilla/EventDispatcher.h"
     17 #include "mozilla/RestyleManager.h"
     18 #include "mozilla/ServoBindings.h"
     19 #include "mozilla/StyleAnimationValue.h"
     20 #include "mozilla/dom/Document.h"
     21 #include "mozilla/dom/DocumentTimeline.h"
     22 #include "mozilla/dom/Element.h"
     23 #include "nsAnimationManager.h"
     24 #include "nsCSSPropertyIDSet.h"
     25 #include "nsCSSProps.h"
     26 #include "nsDisplayList.h"
     27 #include "nsIContent.h"
     28 #include "nsIFrame.h"
     29 #include "nsRFPService.h"
     30 #include "nsStyleChangeList.h"
     31 
     32 using mozilla::dom::CSSTransition;
     33 using mozilla::dom::DocumentTimeline;
     34 using mozilla::dom::KeyframeEffect;
     35 
     36 using namespace mozilla;
     37 using namespace mozilla::css;
     38 
     39 bool nsTransitionManager::UpdateTransitions(
     40    dom::Element* aElement, const PseudoStyleRequest& aPseudoRequest,
     41    const ComputedStyle& aOldStyle, const ComputedStyle& aNewStyle) {
     42  if (mPresContext->Medium() == nsGkAtoms::print) {
     43    // For print or print preview, ignore transitions.
     44    return false;
     45  }
     46 
     47  MOZ_ASSERT(mPresContext->IsDynamic());
     48  if (aNewStyle.StyleDisplay()->mDisplay == StyleDisplay::None) {
     49    StopAnimationsForElement(aElement, aPseudoRequest);
     50    return false;
     51  }
     52 
     53  auto* collection = CSSTransitionCollection::Get(aElement, aPseudoRequest);
     54  return DoUpdateTransitions(*aNewStyle.StyleUIReset(), aElement,
     55                             aPseudoRequest, collection, aOldStyle, aNewStyle);
     56 }
     57 
     58 // This function expands the shorthands and "all" keyword specified in
     59 // transition-property, and then execute |aHandler| on the expanded longhand.
     60 // |aHandler| should be a lamda function which accepts NonCustomCSSPropertyId.
     61 template <typename T>
     62 static void ExpandTransitionProperty(const StyleTransitionProperty& aProperty,
     63                                     T aHandler) {
     64  switch (aProperty.tag) {
     65    case StyleTransitionProperty::Tag::Unsupported:
     66      break;
     67    case StyleTransitionProperty::Tag::Custom: {
     68      auto property =
     69          CSSPropertyId::FromCustomName(aProperty.AsCustom().AsAtom());
     70      aHandler(property);
     71      break;
     72    }
     73    case StyleTransitionProperty::Tag::NonCustom: {
     74      NonCustomCSSPropertyId id =
     75          NonCustomCSSPropertyId(aProperty.AsNonCustom()._0);
     76      if (nsCSSProps::IsShorthand(id)) {
     77        CSSPROPS_FOR_SHORTHAND_SUBPROPERTIES(subprop, id,
     78                                             CSSEnabledState::ForAllContent) {
     79          CSSPropertyId property(*subprop);
     80          aHandler(property);
     81        }
     82      } else {
     83        CSSPropertyId property(id);
     84        aHandler(property);
     85      }
     86      break;
     87    }
     88  }
     89 }
     90 
     91 bool nsTransitionManager::DoUpdateTransitions(
     92    const nsStyleUIReset& aStyle, dom::Element* aElement,
     93    const PseudoStyleRequest& aPseudoRequest,
     94    CSSTransitionCollection*& aElementTransitions,
     95    const ComputedStyle& aOldStyle, const ComputedStyle& aNewStyle) {
     96  MOZ_ASSERT(!aElementTransitions || &aElementTransitions->mElement == aElement,
     97             "Element mismatch");
     98 
     99  // Per http://lists.w3.org/Archives/Public/www-style/2009Aug/0109.html
    100  // I'll consider only the transitions from the number of items in
    101  // 'transition-property' on down, and later ones will override earlier
    102  // ones (tracked using |propertiesChecked|).
    103  bool startedAny = false;
    104  AnimatedPropertyIDSet propertiesChecked;
    105  for (uint32_t i = aStyle.mTransitionPropertyCount; i--;) {
    106    const float delay = aStyle.GetTransitionDelay(i).ToMilliseconds();
    107 
    108    // The spec says a negative duration is treated as zero.
    109    const float duration =
    110        std::max(aStyle.GetTransitionDuration(i).ToMilliseconds(), 0.0f);
    111 
    112    // If the combined duration of this transition is 0 or less we won't start a
    113    // transition, we can avoid even looking at transition-property if we're the
    114    // last one.
    115    if (i == 0 && delay + duration <= 0.0f) {
    116      continue;
    117    }
    118 
    119    const auto behavior = aStyle.GetTransitionBehavior(i);
    120    ExpandTransitionProperty(
    121        aStyle.GetTransitionProperty(i), [&](const CSSPropertyId& aProperty) {
    122          // We might have something to transition.  See if
    123          // any of the properties in question changed and
    124          // are animatable.
    125          startedAny |= ConsiderInitiatingTransition(
    126              aProperty, aStyle, i, delay, duration, behavior, aElement,
    127              aPseudoRequest, aElementTransitions, aOldStyle, aNewStyle,
    128              propertiesChecked);
    129        });
    130  }
    131 
    132  // Stop any transitions for properties that are no longer in
    133  // 'transition-property', including finished transitions.
    134  // Also stop any transitions (and remove any finished transitions)
    135  // for properties that just changed (and are still in the set of
    136  // properties to transition), but for which we didn't just start the
    137  // transition.  This can happen delay and duration are both zero, or
    138  // because the new value is not interpolable.
    139  if (aElementTransitions) {
    140    const bool checkProperties = !aStyle.GetTransitionProperty(0).IsAll();
    141    AnimatedPropertyIDSet allTransitionProperties;
    142    if (checkProperties) {
    143      for (uint32_t i = aStyle.mTransitionPropertyCount; i-- != 0;) {
    144        ExpandTransitionProperty(aStyle.GetTransitionProperty(i),
    145                                 [&](const CSSPropertyId& aProperty) {
    146                                   allTransitionProperties.AddProperty(
    147                                       aProperty.ToPhysical(aNewStyle));
    148                                 });
    149      }
    150    }
    151 
    152    OwningCSSTransitionPtrArray& animations = aElementTransitions->mAnimations;
    153    size_t i = animations.Length();
    154    MOZ_ASSERT(i != 0, "empty transitions list?");
    155    AnimationValue currentValue;
    156    do {
    157      --i;
    158      CSSTransition* anim = animations[i];
    159      const CSSPropertyId& property = anim->TransitionProperty();
    160      if (
    161          // Properties no longer in `transition-property`.
    162          (checkProperties && !allTransitionProperties.HasProperty(property)) ||
    163          // Properties whose computed values changed but for which we did not
    164          // start a new transition (because delay and duration are both zero,
    165          // or because the new value is not interpolable); a new transition
    166          // would have anim->ToValue() matching currentValue.
    167          !Servo_ComputedValues_TransitionValueMatches(
    168              &aNewStyle, &property, anim->ToValue().mServo.get())) {
    169        // Stop the transition.
    170        DoCancelTransition(aElement, aPseudoRequest, aElementTransitions, i);
    171      }
    172    } while (i != 0);
    173  }
    174 
    175  return startedAny;
    176 }
    177 
    178 static Keyframe& AppendKeyframe(double aOffset, const CSSPropertyId& aProperty,
    179                                AnimationValue&& aValue,
    180                                nsTArray<Keyframe>& aKeyframes) {
    181  Keyframe& frame = *aKeyframes.AppendElement();
    182  frame.mOffset.emplace(aOffset);
    183  MOZ_ASSERT(aValue.mServo);
    184  RefPtr<StyleLockedDeclarationBlock> decl =
    185      Servo_AnimationValue_Uncompute(aValue.mServo).Consume();
    186  frame.mPropertyValues.AppendElement(
    187      PropertyValuePair(aProperty, std::move(decl)));
    188  return frame;
    189 }
    190 
    191 static nsTArray<Keyframe> GetTransitionKeyframes(const CSSPropertyId& aProperty,
    192                                                 AnimationValue&& aStartValue,
    193                                                 AnimationValue&& aEndValue) {
    194  nsTArray<Keyframe> keyframes(2);
    195 
    196  AppendKeyframe(0.0, aProperty, std::move(aStartValue), keyframes);
    197  AppendKeyframe(1.0, aProperty, std::move(aEndValue), keyframes);
    198 
    199  return keyframes;
    200 }
    201 
    202 using ReplacedTransitionProperties =
    203    CSSTransition::ReplacedTransitionProperties;
    204 static Maybe<ReplacedTransitionProperties> GetReplacedTransitionProperties(
    205    const CSSTransition& aTransition,
    206    const DocumentTimeline* aTimelineToMatch) {
    207  Maybe<ReplacedTransitionProperties> result;
    208 
    209  if (!aTransition.HasCurrentEffect()) {
    210    return result;
    211  }
    212 
    213  // Transition needs to be running on the same timeline.
    214  if (aTransition.GetTimeline() != aTimelineToMatch) {
    215    return result;
    216  }
    217 
    218  auto startTime = aTransition.GetStartTime();
    219  if (startTime.IsNull() && !aTransition.GetPendingReadyTime().IsNull()) {
    220    startTime =
    221        aTimelineToMatch->ToTimelineTime(aTransition.GetPendingReadyTime());
    222  }
    223 
    224  if (startTime.IsNull()) {
    225    return result;
    226  }
    227 
    228  // The transition needs to have a keyframe effect.
    229  const KeyframeEffect* keyframeEffect =
    230      aTransition.GetEffect() ? aTransition.GetEffect()->AsKeyframeEffect()
    231                              : nullptr;
    232  if (!keyframeEffect) {
    233    return result;
    234  }
    235 
    236  // The keyframe effect needs to be a simple transition of the original
    237  // transition property (i.e. not replaced with something else).
    238  if (keyframeEffect->Properties().Length() != 1 ||
    239      keyframeEffect->Properties()[0].mSegments.Length() != 1 ||
    240      keyframeEffect->Properties()[0].mProperty !=
    241          aTransition.TransitionProperty()) {
    242    return result;
    243  }
    244 
    245  const AnimationPropertySegment& segment =
    246      keyframeEffect->Properties()[0].mSegments[0];
    247 
    248  result.emplace(ReplacedTransitionProperties(
    249      {startTime.Value(), aTransition.PlaybackRate(),
    250       keyframeEffect->SpecifiedTiming(), segment.mTimingFunction,
    251       segment.mFromValue, segment.mToValue}));
    252 
    253  return result;
    254 }
    255 
    256 bool nsTransitionManager::ConsiderInitiatingTransition(
    257    const CSSPropertyId& aProperty, const nsStyleUIReset& aStyle,
    258    uint32_t aTransitionIndex, float aDelay, float aDuration,
    259    mozilla::StyleTransitionBehavior aBehavior, dom::Element* aElement,
    260    const PseudoStyleRequest& aPseudoRequest,
    261    CSSTransitionCollection*& aElementTransitions,
    262    const ComputedStyle& aOldStyle, const ComputedStyle& aNewStyle,
    263    AnimatedPropertyIDSet& aPropertiesChecked) {
    264  // IsShorthand itself will assert if aProperty is not a property.
    265  MOZ_ASSERT(aProperty.IsCustom() || !nsCSSProps::IsShorthand(aProperty.mId),
    266             "property out of range");
    267  NS_ASSERTION(
    268      !aElementTransitions || &aElementTransitions->mElement == aElement,
    269      "Element mismatch");
    270 
    271  CSSPropertyId property = aProperty.ToPhysical(aNewStyle);
    272 
    273  // A later item in transition-property already specified a transition for
    274  // this property, so we ignore this one.
    275  //
    276  // See http://lists.w3.org/Archives/Public/www-style/2009Aug/0109.html .
    277  if (aPropertiesChecked.HasProperty(property)) {
    278    return false;
    279  }
    280 
    281  aPropertiesChecked.AddProperty(property);
    282 
    283  if (aDuration + aDelay <= 0.0f) {
    284    return false;
    285  }
    286 
    287  size_t currentIndex = nsTArray<KeyframeEffect>::NoIndex;
    288  const auto* oldTransition = [&]() -> const CSSTransition* {
    289    if (!aElementTransitions) {
    290      return nullptr;
    291    }
    292    const OwningCSSTransitionPtrArray& animations =
    293        aElementTransitions->mAnimations;
    294    for (size_t i = 0, i_end = animations.Length(); i < i_end; ++i) {
    295      if (animations[i]->TransitionProperty() == property) {
    296        currentIndex = i;
    297        return animations[i];
    298      }
    299    }
    300    return nullptr;
    301  }();
    302 
    303  // For compositor animations, |aOldStyle| may have out-of-date transition
    304  // rules, and it may be equal to the |endValue| of a reversing transition by
    305  // accidentally. This causes Servo_ComputedValues_ShouldTransition() to return
    306  // an incorrect result. Therefore, we have to recompute the current value if
    307  // this transition is running on the compositor, to make sure we create the
    308  // transition properly. Here, we pre-compute the progress and collect the
    309  // necessary info, so Servo_ComputedValues_ShouldTransition() could compute
    310  // the current value if needed.
    311  // FIXME: Bug 1634945. We should use the last value from the compositor as the
    312  // current value.
    313  Maybe<ReplacedTransitionProperties> replacedTransitionProperties;
    314  Maybe<double> progress;
    315  if (oldTransition) {
    316    // If this new transition is replacing an existing transition, we store
    317    // select parameters from the replaced transition so that later, once all
    318    // scripts have run, we can update the start value of the transition using
    319    // TimeStamp::Now(). This allows us to avoid a large jump when starting a
    320    // new transition when the main thread lags behind the compositor.
    321    //
    322    // Note: We compute this before calling
    323    // Servo_ComputedValues_ShouldTransition() so we can reuse it for computing
    324    // the current value and setting the replaced transition properties later in
    325    // this function.
    326    const dom::DocumentTimeline* timeline = aElement->OwnerDoc()->Timeline();
    327    replacedTransitionProperties =
    328        GetReplacedTransitionProperties(*oldTransition, timeline);
    329    progress = replacedTransitionProperties.andThen(
    330        [&](const ReplacedTransitionProperties& aProperties) {
    331          const dom::AnimationTimeline* timeline = oldTransition->GetTimeline();
    332          MOZ_ASSERT(timeline);
    333          return CSSTransition::ComputeTransformedProgress(*timeline,
    334                                                           aProperties);
    335        });
    336  }
    337 
    338  AnimationValue startValue, endValue;
    339  const StyleShouldTransitionResult result =
    340      Servo_ComputedValues_ShouldTransition(
    341          &aOldStyle, &aNewStyle, &property, aBehavior,
    342          oldTransition ? oldTransition->ToValue().mServo.get() : nullptr,
    343          replacedTransitionProperties
    344              ? replacedTransitionProperties->mFromValue.mServo.get()
    345              : nullptr,
    346          // Note: It's possible to replace the keyframes by Web Animations API,
    347          // so we have to pass the mToValue from the keyframe segment, to make
    348          // sure this value is aligned with mFromValue.
    349          replacedTransitionProperties
    350              ? replacedTransitionProperties->mToValue.mServo.get()
    351              : nullptr,
    352          progress.ptrOr(nullptr), &startValue.mServo, &endValue.mServo);
    353 
    354  // If we got a style change that changed the value to the endpoint
    355  // of the currently running transition, we don't want to interrupt
    356  // its timing function.
    357  // This needs to be before the !shouldAnimate && haveCurrentTransition
    358  // case below because we might be close enough to the end of the
    359  // transition that the current value rounds to the final value.  In
    360  // this case, we'll end up with shouldAnimate as false (because
    361  // there's no value change), but we need to return early here rather
    362  // than cancel the running transition because shouldAnimate is false!
    363  //
    364  // Likewise, if we got a style change that changed the value to the
    365  // endpoint of our finished transition, we also don't want to start
    366  // a new transition for the reasons described in
    367  // https://lists.w3.org/Archives/Public/www-style/2015Jan/0444.html .
    368  if (result.old_transition_value_matches) {
    369    // GetAnimationRule already called RestyleForAnimation.
    370    return false;
    371  }
    372 
    373  if (!result.should_animate) {
    374    if (oldTransition) {
    375      // We're in the middle of a transition, and just got a non-transition
    376      // style change to something that we can't animate.  This might happen
    377      // because we got a non-transition style change changing to the current
    378      // in-progress value (which is particularly easy to cause when we're
    379      // currently in the 'transition-delay').  It also might happen because we
    380      // just got a style change to a value that can't be interpolated.
    381      DoCancelTransition(aElement, aPseudoRequest, aElementTransitions,
    382                         currentIndex);
    383    }
    384    return false;
    385  }
    386 
    387  AnimationValue startForReversingTest = startValue;
    388  double reversePortion = 1.0;
    389 
    390  // If the new transition reverses an existing one, we'll need to
    391  // handle the timing differently.
    392  if (oldTransition && oldTransition->HasCurrentEffect() &&
    393      oldTransition->StartForReversingTest() == endValue) {
    394    // Compute the appropriate negative transition-delay such that right
    395    // now we'd end up at the current position.
    396    double valuePortion =
    397        oldTransition->CurrentValuePortion() * oldTransition->ReversePortion() +
    398        (1.0 - oldTransition->ReversePortion());
    399    // A timing function with negative y1 (or y2!) might make
    400    // valuePortion negative.  In this case, we still want to apply our
    401    // reversing logic based on relative distances, not make duration
    402    // negative.
    403    if (valuePortion < 0.0) {
    404      valuePortion = -valuePortion;
    405    }
    406    // A timing function with y2 (or y1!) greater than one might
    407    // advance past its terminal value.  It's probably a good idea to
    408    // clamp valuePortion to be at most one to preserve the invariant
    409    // that a transition will complete within at most its specified
    410    // time.
    411    if (valuePortion > 1.0) {
    412      valuePortion = 1.0;
    413    }
    414 
    415    // Negative delays are essentially part of the transition
    416    // function, so reduce them along with the duration, but don't
    417    // reduce positive delays.
    418    if (aDelay < 0.0f && std::isfinite(aDelay)) {
    419      aDelay *= valuePortion;
    420    }
    421 
    422    if (std::isfinite(aDuration)) {
    423      aDuration *= valuePortion;
    424    }
    425 
    426    startForReversingTest = oldTransition->ToValue();
    427    reversePortion = valuePortion;
    428  }
    429 
    430  TimingParams timing = TimingParamsFromCSSParams(
    431      Some(aDuration), aDelay, 1.0 /* iteration count */,
    432      StyleAnimationDirection::Normal, StyleAnimationFillMode::Backwards);
    433 
    434  const StyleComputedTimingFunction& tf =
    435      aStyle.GetTransitionTimingFunction(aTransitionIndex);
    436  if (!tf.IsLinearKeyword()) {
    437    timing.SetTimingFunction(Some(tf));
    438  }
    439 
    440  RefPtr<CSSTransition> transition = DoCreateTransition(
    441      property, aElement, aPseudoRequest, aNewStyle, aElementTransitions,
    442      std::move(timing), std::move(startValue), std::move(endValue),
    443      std::move(startForReversingTest), reversePortion);
    444  if (!transition) {
    445    return false;
    446  }
    447 
    448  OwningCSSTransitionPtrArray& transitions = aElementTransitions->mAnimations;
    449 #ifdef DEBUG
    450  for (size_t i = 0, i_end = transitions.Length(); i < i_end; ++i) {
    451    MOZ_ASSERT(
    452        i == currentIndex || transitions[i]->TransitionProperty() != property,
    453        "duplicate transitions for property");
    454  }
    455 #endif
    456  if (oldTransition) {
    457    if (replacedTransitionProperties) {
    458      transition->SetReplacedTransition(
    459          std::move(replacedTransitionProperties.ref()));
    460    }
    461 
    462    transitions[currentIndex]->CancelFromStyle(PostRestyleMode::IfNeeded);
    463    oldTransition = nullptr;  // Clear pointer so it doesn't dangle
    464    transitions[currentIndex] = transition;
    465  } else {
    466    // XXX(Bug 1631371) Check if this should use a fallible operation as it
    467    // pretended earlier.
    468    transitions.AppendElement(transition);
    469  }
    470 
    471  if (auto* effectSet = EffectSet::Get(aElement, aPseudoRequest)) {
    472    effectSet->UpdateAnimationGeneration(mPresContext);
    473  }
    474 
    475  return true;
    476 }
    477 
    478 already_AddRefed<CSSTransition> nsTransitionManager::DoCreateTransition(
    479    const CSSPropertyId& aProperty, dom::Element* aElement,
    480    const PseudoStyleRequest& aPseudoRequest,
    481    const mozilla::ComputedStyle& aNewStyle,
    482    CSSTransitionCollection*& aElementTransitions, TimingParams&& aTiming,
    483    AnimationValue&& aStartValue, AnimationValue&& aEndValue,
    484    AnimationValue&& aStartForReversingTest, double aReversePortion) {
    485  dom::DocumentTimeline* timeline = aElement->OwnerDoc()->Timeline();
    486  KeyframeEffectParams effectOptions;
    487  auto keyframeEffect = MakeRefPtr<KeyframeEffect>(
    488      aElement->OwnerDoc(), OwningAnimationTarget(aElement, aPseudoRequest),
    489      std::move(aTiming), effectOptions);
    490 
    491  keyframeEffect->SetKeyframes(
    492      GetTransitionKeyframes(aProperty, std::move(aStartValue),
    493                             std::move(aEndValue)),
    494      &aNewStyle, timeline);
    495 
    496  if (NS_WARN_IF(MOZ_UNLIKELY(!keyframeEffect->IsValidTransition()))) {
    497    return nullptr;
    498  }
    499 
    500  auto animation = MakeRefPtr<CSSTransition>(
    501      mPresContext->Document()->GetScopeObject(), aProperty);
    502  animation->SetOwningElement(OwningElementRef(*aElement, aPseudoRequest));
    503  animation->SetTimelineNoUpdate(timeline);
    504  animation->SetCreationSequence(
    505      mPresContext->RestyleManager()->GetAnimationGeneration());
    506  animation->SetEffectFromStyle(keyframeEffect);
    507  animation->SetReverseParameters(std::move(aStartForReversingTest),
    508                                  aReversePortion);
    509  animation->PlayFromStyle();
    510 
    511  if (!aElementTransitions) {
    512    aElementTransitions =
    513        &aElement->EnsureAnimationData().EnsureTransitionCollection(
    514            *aElement, aPseudoRequest);
    515    if (!aElementTransitions->isInList()) {
    516      AddElementCollection(aElementTransitions);
    517    }
    518  }
    519  return animation.forget();
    520 }
    521 
    522 void nsTransitionManager::DoCancelTransition(
    523    dom::Element* aElement, const PseudoStyleRequest& aPseudoRequest,
    524    CSSTransitionCollection*& aElementTransitions, size_t aIndex) {
    525  MOZ_ASSERT(aElementTransitions);
    526  OwningCSSTransitionPtrArray& transitions = aElementTransitions->mAnimations;
    527  CSSTransition* transition = transitions[aIndex];
    528 
    529  if (transition->HasCurrentEffect()) {
    530    if (auto* effectSet = EffectSet::Get(aElement, aPseudoRequest)) {
    531      effectSet->UpdateAnimationGeneration(mPresContext);
    532    }
    533  }
    534  transition->CancelFromStyle(PostRestyleMode::IfNeeded);
    535  transitions.RemoveElementAt(aIndex);
    536 
    537  if (transitions.IsEmpty()) {
    538    aElementTransitions->Destroy();
    539    // |aElementTransitions| is now a dangling pointer!
    540    aElementTransitions = nullptr;
    541  }
    542 }