Implement CSS transitions: handle starting and animation of the transitions. (Bug 435441) r=bzbarsky sr=roc

This commit is contained in:
L. David Baron 2009-10-07 20:22:42 -07:00
parent 4c250ab973
commit 2ccbe80f89
28 changed files with 2266 additions and 14 deletions

View File

@ -2691,6 +2691,16 @@ nsGenericElement::UnbindFromTree(PRBool aDeep, PRBool aNullParent)
document->ClearBoxObjectFor(this);
}
// Ensure that CSS transitions don't continue on an element at a
// different place in the tree (even if reinserted before next
// animation refresh).
// FIXME: Need a test for this.
if (HasFlag(NODE_HAS_PROPERTIES)) {
DeleteProperty(nsGkAtoms::transitionsOfBeforeProperty);
DeleteProperty(nsGkAtoms::transitionsOfAfterProperty);
DeleteProperty(nsGkAtoms::transitionsProperty);
}
// Unset this since that's what the old code effectively did.
UnsetFlags(NODE_FORCE_XBL_BINDINGS);

View File

@ -1674,6 +1674,9 @@ GK_ATOM(preTransformBBoxProperty, "PreTransformBBoxProperty") // nsRect*
GK_ATOM(rowUnpaginatedHeightProperty, "RowUnpaginatedHeightProperty") // nscoord*
GK_ATOM(tabWidthProperty, "TabWidthProperty") // nsTArray<TabSetting>* array of tab widths
GK_ATOM(tableBCProperty, "TableBCProperty") // table border collapsing info (e.g. damage area, table border widths)
GK_ATOM(transitionsProperty, "TransitionsProperty") // FrameTransitions*
GK_ATOM(transitionsOfBeforeProperty, "TransitionsOfBeforeProperty") // FrameTransitions*
GK_ATOM(transitionsOfAfterProperty, "TransitionsOfAfterProperty") // FrameTransitions*
GK_ATOM(usedMarginProperty, "UsedMarginProperty") // nsMargin*
GK_ATOM(usedPaddingProperty, "UsedPaddingProperty") // nsMargin*
GK_ATOM(viewProperty, "ViewProperty")

View File

@ -81,6 +81,7 @@ EXPORTS = \
nsISMILAttr.h \
nsSMILAnimationController.h \
nsSMILCompositorTable.h \
nsSMILKeySpline.h \
nsSMILTimeContainer.h \
nsSMILTypes.h \
$(NULL)

View File

@ -47,15 +47,17 @@
const double nsSMILKeySpline::kSampleStepSize =
1.0 / double(kSplineTableSize - 1);
nsSMILKeySpline::nsSMILKeySpline(double aX1,
double aY1,
double aX2,
double aY2)
: mX1(aX1),
mY1(aY1),
mX2(aX2),
mY2(aY2)
void
nsSMILKeySpline::Init(double aX1,
double aY1,
double aX2,
double aY2)
{
mX1 = aX1;
mY1 = aY1;
mX2 = aX2;
mY2 = aY2;
if (mX1 != mY1 || mX2 != mY2)
CalcSampleValues();
}

View File

@ -44,6 +44,8 @@
class nsSMILKeySpline
{
public:
nsSMILKeySpline() { /* caller must call Init later */ }
/**
* Creates a new key spline control point description.
*
@ -51,7 +53,13 @@ public:
* SMILANIM 3.2.3. They must each be in the range 0.0 <= x <= 1.0
*/
nsSMILKeySpline(double aX1, double aY1,
double aX2, double aY2);
double aX2, double aY2)
{
Init(aX1, aY1, aX2, aY2);
}
void Init(double aX1, double aY1,
double aX2, double aY2);
/**
* Gets the output (y) value for an input (x).
@ -104,10 +112,10 @@ private:
return 3.0 * aA1;
}
const double mX1;
const double mY1;
const double mX2;
const double mY2;
double mX1;
double mY1;
double mX2;
double mY2;
enum { kSplineTableSize = 11 };
double mSampleValues[kSplineTableSize];

View File

@ -81,6 +81,7 @@ EXPORTS = \
nsLayoutUtils.h \
nsPresContext.h \
nsPresState.h \
nsRefreshDriver.h \
nsStyleChangeList.h \
nsStyleConsts.h \
$(NULL)
@ -107,6 +108,7 @@ CPPSRCS = \
nsPresShell.cpp \
nsPresState.cpp \
nsQuoteList.cpp \
nsRefreshDriver.cpp \
nsStyleChangeList.cpp \
nsStyleSheetService.cpp \
$(NULL)

View File

@ -68,6 +68,7 @@ class nsIFrame;
struct nsGenConInitializer;
class ChildIterator;
class nsICSSAnonBoxPseudo;
class nsPageContentFrame;
struct nsFindFrameHint
{

View File

@ -90,6 +90,7 @@
#include "nsLayoutUtils.h"
#include "nsAutoPtr.h"
#include "imgIRequest.h"
#include "nsTransitionManager.h"
#include "nsFrameManager.h"
#ifdef ACCESSIBILITY
@ -926,6 +927,33 @@ nsFrameManager::DebugVerifyStyleTree(nsIFrame* aFrame)
#endif // DEBUG
// aContent must be the content for the frame in question, which may be
// :before/:after content
static void
TryStartingTransition(nsPresContext *aPresContext, nsIContent *aContent,
nsStyleContext *aOldStyleContext,
nsRefPtr<nsStyleContext> *aNewStyleContext /* inout */)
{
// Notify the transition manager, and if it starts a transition,
// it will give us back a transition-covering style rule which
// we'll use to get *another* style context. We want to ignore
// any already-running transitions, but cover up any that we're
// currently starting with their start value so we don't start
// them again for descendants that inherit that value.
nsCOMPtr<nsIStyleRule> coverRule =
aPresContext->TransitionManager()->StyleContextChanged(
aContent, aOldStyleContext, *aNewStyleContext);
if (coverRule) {
nsCOMArray<nsIStyleRule> rules;
rules.AppendObject(coverRule);
*aNewStyleContext = aPresContext->StyleSet()->ResolveStyleForRules(
(*aNewStyleContext)->GetParent(),
(*aNewStyleContext)->GetPseudoType(),
(*aNewStyleContext)->GetRuleNode(),
rules);
}
}
nsresult
nsFrameManager::ReParentStyleContext(nsIFrame* aFrame)
{
@ -971,6 +999,16 @@ nsFrameManager::ReParentStyleContext(nsIFrame* aFrame)
newParentContext);
if (newContext) {
if (newContext != oldContext) {
// We probably don't want to initiate transitions from
// ReParentStyleContext, since we call it during frame
// construction rather than in response to dynamic changes.
// Also see the comment at the start of
// nsTransitionManager::ConsiderStartingTransition.
#if 0
TryStartingTransition(presContext, aFrame->GetContent(),
oldContext, &newContext);
#endif
// Make sure to call CalcStyleDifference so that the new context ends
// up resolving all the structs the old context resolved.
nsChangeHint styleChange = oldContext->CalcStyleDifference(newContext);
@ -1260,6 +1298,9 @@ nsFrameManager::ReResolveStyleContext(nsPresContext *aPresContext,
}
if (newContext != oldContext) {
TryStartingTransition(aPresContext, aFrame->GetContent(),
oldContext, &newContext);
aMinChange = CaptureChange(oldContext, newContext, aFrame,
content, aChangeList, aMinChange,
assumeDifferenceHint);

View File

@ -94,6 +94,7 @@
#include "nsIPrivateDOMEvent.h"
#include "nsIDOMEventTarget.h"
#include "nsObjectFrame.h"
#include "nsTransitionManager.h"
#ifdef MOZ_SMIL
#include "nsSMILAnimationController.h"
@ -258,6 +259,8 @@ nsPresContext::~nsPresContext()
NS_PRECONDITION(!mShell, "Presshell forgot to clear our mShell pointer");
SetShell(nsnull);
delete mTransitionManager;
if (mEventManager) {
// unclear if these are needed, but can't hurt
mEventManager->NotifyDestroyPresContext(this);
@ -869,6 +872,8 @@ nsPresContext::Init(nsIDeviceContext* aDeviceContext)
NS_ADDREF(mEventManager);
mTransitionManager = new nsTransitionManager(this);
mLangService = do_GetService(NS_LANGUAGEATOMSERVICE_CONTRACTID);
// Register callbacks so we're notified when the preferences change

View File

@ -72,6 +72,7 @@
#include "nsContentUtils.h"
#include "nsIWidget.h"
#include "mozilla/TimeStamp.h"
#include "nsRefreshDriver.h"
class nsImageLoader;
#ifdef IBMBIDI
@ -101,6 +102,7 @@ class gfxUserFontSet;
class nsUserFontSet;
struct nsFontFaceRuleContainer;
class nsObjectFrame;
class nsTransitionManager;
#ifdef MOZ_REFLOW_PERF
class nsIRenderingContext;
@ -227,6 +229,16 @@ public:
nsFrameManager* FrameManager()
{ return GetPresShell()->FrameManager(); }
nsTransitionManager* TransitionManager() { return mTransitionManager; }
nsRefreshDriver* RefreshDriver() { return &mRefreshDriver; }
static nsPresContext* FromRefreshDriver(nsRefreshDriver* aRefreshDriver) {
return reinterpret_cast<nsPresContext*>(
reinterpret_cast<char*>(aRefreshDriver) -
offsetof(nsPresContext, mRefreshDriver));
}
#endif
/**
@ -945,6 +957,8 @@ protected:
// from gfx back to layout.
nsIEventStateManager* mEventManager; // [STRONG]
nsILookAndFeel* mLookAndFeel; // [STRONG]
nsRefreshDriver mRefreshDriver;
nsTransitionManager* mTransitionManager; // owns; it aggregates our refcount
nsIAtom* mMedium; // initialized by subclass ctors;
// weak pointer to static atom

View File

@ -0,0 +1,219 @@
/* vim: set shiftwidth=2 tabstop=8 autoindent cindent expandtab: */
/* ***** BEGIN LICENSE BLOCK *****
* Version: MPL 1.1/GPL 2.0/LGPL 2.1
*
* The contents of this file are subject to the Mozilla Public License Version
* 1.1 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
* http://www.mozilla.org/MPL/
*
* Software distributed under the License is distributed on an "AS IS" basis,
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
* for the specific language governing rights and limitations under the
* License.
*
* The Original Code is nsRefreshDriver.
*
* The Initial Developer of the Original Code is the Mozilla Foundation.
* Portions created by the Initial Developer are Copyright (C) 2009
* the Initial Developer. All Rights Reserved.
*
* Contributor(s):
* L. David Baron <dbaron@dbaron.org>, Mozilla Corporation (original author)
*
* Alternatively, the contents of this file may be used under the terms of
* either the GNU General Public License Version 2 or later (the "GPL"), or
* the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
* in which case the provisions of the GPL or the LGPL are applicable instead
* of those above. If you wish to allow use of your version of this file only
* under the terms of either the GPL or the LGPL, and not to allow others to
* use your version of this file under the terms of the MPL, indicate your
* decision by deleting the provisions above and replace them with the notice
* and other provisions required by the GPL or the LGPL. If you do not delete
* the provisions above, a recipient may use your version of this file under
* the terms of any one of the MPL, the GPL or the LGPL.
*
* ***** END LICENSE BLOCK ***** */
/*
* Code to notify things that animate before a refresh, at an appropriate
* refresh rate. (Perhaps temporary, until replaced by compositor.)
*/
#include "nsRefreshDriver.h"
#include "nsPresContext.h"
#include "nsComponentManagerUtils.h"
#include "prlog.h"
/*
* TODO:
* Once this is hooked in to suppressing updates when the presentation
* is not visible, we need to hook it up to FlushPendingNotifications so
* that we flush when necessary.
*/
#define REFRESH_INTERVAL_MILLISECONDS 20
using mozilla::TimeStamp;
nsRefreshDriver::nsRefreshDriver()
{
}
nsRefreshDriver::~nsRefreshDriver()
{
NS_ABORT_IF_FALSE(ObserverCount() == 0,
"observers should have unregistered");
NS_ABORT_IF_FALSE(!mTimer, "timer should be gone");
}
TimeStamp
nsRefreshDriver::MostRecentRefresh() const
{
const_cast<nsRefreshDriver*>(this)->EnsureTimerStarted();
return mMostRecentRefresh;
}
PRBool
nsRefreshDriver::AddRefreshObserver(nsARefreshObserver *aObserver,
mozFlushType aFlushType)
{
ObserverArray& array = ArrayFor(aFlushType);
PRBool success = array.AppendElement(aObserver) != nsnull;
EnsureTimerStarted();
return success;
}
PRBool
nsRefreshDriver::RemoveRefreshObserver(nsARefreshObserver *aObserver,
mozFlushType aFlushType)
{
ObserverArray& array = ArrayFor(aFlushType);
PRBool success = array.RemoveElement(aObserver);
if (ObserverCount() == 0) {
StopTimer();
}
return success;
}
void
nsRefreshDriver::EnsureTimerStarted()
{
if (mTimer) {
// It's already been started.
return;
}
UpdateMostRecentRefresh();
mTimer = do_CreateInstance(NS_TIMER_CONTRACTID);
if (!mTimer) {
return;
}
nsresult rv = mTimer->InitWithCallback(this, REFRESH_INTERVAL_MILLISECONDS,
nsITimer::TYPE_REPEATING_SLACK);
if (NS_FAILED(rv)) {
mTimer = nsnull;
}
}
void
nsRefreshDriver::StopTimer()
{
if (!mTimer) {
return;
}
mTimer->Cancel();
mTimer = nsnull;
}
PRUint32
nsRefreshDriver::ObserverCount() const
{
PRUint32 sum = 0;
for (PRUint32 i = 0; i < NS_ARRAY_LENGTH(mObservers); ++i) {
sum += mObservers[i].Length();
}
return sum;
}
void
nsRefreshDriver::UpdateMostRecentRefresh()
{
mMostRecentRefresh = TimeStamp::Now();
}
nsRefreshDriver::ObserverArray&
nsRefreshDriver::ArrayFor(mozFlushType aFlushType)
{
switch (aFlushType) {
case Flush_Style:
return mObservers[0];
case Flush_Layout:
return mObservers[1];
case Flush_Display:
return mObservers[2];
default:
NS_ABORT_IF_FALSE(PR_FALSE, "bad flush type");
return *static_cast<ObserverArray*>(nsnull);
}
}
/*
* nsISupports implementation
*/
NS_IMPL_ADDREF_USING_AGGREGATOR(nsRefreshDriver,
nsPresContext::FromRefreshDriver(this))
NS_IMPL_RELEASE_USING_AGGREGATOR(nsRefreshDriver,
nsPresContext::FromRefreshDriver(this))
NS_IMPL_QUERY_INTERFACE1(nsRefreshDriver, nsITimerCallback)
/*
* nsITimerCallback implementation
*/
NS_IMETHODIMP
nsRefreshDriver::Notify(nsITimer *aTimer)
{
UpdateMostRecentRefresh();
nsPresContext *presContext = nsPresContext::FromRefreshDriver(this);
nsCOMPtr<nsIPresShell> presShell = presContext->GetPresShell();
if (!presShell) {
// Things are being destroyed.
StopTimer();
return NS_OK;
}
for (PRUint32 i = 0; i < NS_ARRAY_LENGTH(mObservers); ++i) {
ObserverArray::EndLimitedIterator etor(mObservers[i]);
while (etor.HasMore()) {
etor.GetNext()->WillRefresh(mMostRecentRefresh);
}
if (i == 0) {
// This is the Flush_Style case.
// FIXME: Maybe we should only flush if the WillRefresh calls did
// something? It's probably ok as-is, though, especially as we
// hook up more things here (or to the replacement of this class).
// FIXME: We should probably flush for other sets of observers
// too. But we should only flush layout once nsRefreshDriver is
// the driver for the interruptible layout timer (and we should
// then Flush_InterruptibleLayout).
presShell->FlushPendingNotifications(Flush_Style);
}
}
if (ObserverCount() == 0) {
StopTimer();
}
return NS_OK;
}

View File

@ -0,0 +1,120 @@
/* vim: set shiftwidth=2 tabstop=8 autoindent cindent expandtab: */
/* ***** BEGIN LICENSE BLOCK *****
* Version: MPL 1.1/GPL 2.0/LGPL 2.1
*
* The contents of this file are subject to the Mozilla Public License Version
* 1.1 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
* http://www.mozilla.org/MPL/
*
* Software distributed under the License is distributed on an "AS IS" basis,
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
* for the specific language governing rights and limitations under the
* License.
*
* The Original Code is nsRefreshDriver.
*
* The Initial Developer of the Original Code is the Mozilla Foundation.
* Portions created by the Initial Developer are Copyright (C) 2009
* the Initial Developer. All Rights Reserved.
*
* Contributor(s):
* L. David Baron <dbaron@dbaron.org>, Mozilla Corporation (original author)
*
* Alternatively, the contents of this file may be used under the terms of
* either the GNU General Public License Version 2 or later (the "GPL"), or
* the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
* in which case the provisions of the GPL or the LGPL are applicable instead
* of those above. If you wish to allow use of your version of this file only
* under the terms of either the GPL or the LGPL, and not to allow others to
* use your version of this file under the terms of the MPL, indicate your
* decision by deleting the provisions above and replace them with the notice
* and other provisions required by the GPL or the LGPL. If you do not delete
* the provisions above, a recipient may use your version of this file under
* the terms of any one of the MPL, the GPL or the LGPL.
*
* ***** END LICENSE BLOCK ***** */
/*
* Code to notify things that animate before a refresh, at an appropriate
* refresh rate. (Perhaps temporary, until replaced by compositor.)
*/
#ifndef nsRefreshDriver_h_
#define nsRefreshDriver_h_
#include "mozilla/TimeStamp.h"
#include "mozFlushType.h"
#include "nsITimer.h"
#include "nsCOMPtr.h"
#include "nsTObserverArray.h"
/**
* An abstract base class to be implemented by callers wanting to be
* notified at refresh times. When nothing needs to be painted, callers
* may not be notified.
*/
class nsARefreshObserver {
public:
virtual void WillRefresh(mozilla::TimeStamp aTime) = 0;
};
/*
* nsRefreshDriver MUST ONLY be constructed as a sub-object of
* nsPresContext (since its reference counting methods forward to the
* pres context of which it is an mRefreshDriver)
*/
class nsRefreshDriver : private nsITimerCallback {
public:
nsRefreshDriver();
~nsRefreshDriver();
/**
* Return the time of the most recent refresh. This is intended to be
* used by callers who want to start an animation now and want to know
* what time to consider the start of the animation. (This helps
* ensure that multiple animations started during the same event off
* the main event loop have the same start time.)
*/
mozilla::TimeStamp MostRecentRefresh() const;
/**
* Add / remove refresh observers. Returns whether the operation
* succeeded.
*
* The flush type affects:
* + the order in which the observers are notified (lowest flush
* type to highest, in order registered)
* + (in the future) which observers are suppressed when the display
* doesn't require current position data or isn't currently
* painting, and, correspondingly, which get notified when there
* is a flush during such suppression
* and it must be either Flush_Style, Flush_Layout, or Flush_Display.
*/
PRBool AddRefreshObserver(nsARefreshObserver *aObserver,
mozFlushType aFlushType);
PRBool RemoveRefreshObserver(nsARefreshObserver *aObserver,
mozFlushType aFlushType);
private:
// nsISupports implementation
NS_DECL_ISUPPORTS_INHERITED
// nsITimerCallback implementation
NS_IMETHOD Notify(nsITimer *aTimer);
typedef nsTObserverArray<nsARefreshObserver*> ObserverArray;
void EnsureTimerStarted();
void StopTimer();
PRUint32 ObserverCount() const;
void UpdateMostRecentRefresh();
ObserverArray& ArrayFor(mozFlushType aFlushType);
nsCOMPtr<nsITimer> mTimer;
mozilla::TimeStamp mMostRecentRefresh; // only valid when mTimer non-null
// separate arrays for each flush type we support
ObserverArray mObservers[3];
};
#endif /* !defined(nsRefreshDriver_h_) */

View File

@ -0,0 +1,4 @@
== transitions-inline-already-wrapped-1.html transitions-inline-ref.html
== transitions-inline-already-wrapped-2.html transitions-inline-ref.html
== transitions-inline-rewrap-1.html transitions-inline-ref.html
== transitions-inline-rewrap-2.html transitions-inline-ref.html

View File

@ -0,0 +1,30 @@
<html class="reftest-wait">
<title>Test for CSS transitions and re-wrapping of inlines</title>
<style type="text/css">
#test { -moz-transition: 5s color linear 200s; }
</style>
<script type="text/javascript">
window.onload = run;
function run() {
var test = document.getElementById("test");
var unused = test.offsetWidth;
// FIXME: It's a bug that we need to do this at all: the way we change
// style data essentially violates style rule immutability because we
// assume that all of the difference calculation will use
// PeekStyleData, which is no longer true with transitions.
// See the FIXME in nsTransitionManager::ConsiderStartingTransition.
unused = getComputedStyle(test, "").color;
test.style.color = "red";
unused = test.offsetWidth;
unused = getComputedStyle(test, "").color;
document.documentElement.removeAttribute("class");
}
</script>
<div style="width: 3em">
<span id="test" style="color: green">
This is some text with a transition.
</span>
</div>

View File

@ -0,0 +1,28 @@
<html class="reftest-wait">
<title>Test for CSS transitions and re-wrapping of inlines</title>
<style type="text/css">
#test { -moz-transition: 20ms color linear 0; }
</style>
<script type="text/javascript">
window.onload = run;
function run() {
var test = document.getElementById("test");
var unused = test.offsetWidth;
unused = getComputedStyle(test, "").color;
test.style.color = "green";
unused = test.offsetWidth;
unused = getComputedStyle(test, "").color;
setTimeout(step2, 100); // give transition time to run
}
function step2() {
document.documentElement.removeAttribute("class");
}
</script>
<div style="width: 3em">
<span id="test" style="color: red">
This is some text with a transition.
</span>
</div>

View File

@ -0,0 +1,7 @@
<html>
<title>Test for CSS transitions and re-wrapping of inlines</title>
<div style="width: 3em">
<span id="test" style="color: green">
This is some text with a transition.
</span>
</div>

View File

@ -0,0 +1,33 @@
<html class="reftest-wait">
<title>Test for CSS transitions and re-wrapping of inlines</title>
<style type="text/css">
#test { -moz-transition: 5s color linear 200s; }
</style>
<script type="text/javascript">
window.onload = run;
function run() {
var test = document.getElementById("test");
var unused = test.offsetWidth;
// FIXME: It's a bug that we need to do this at all: the way we change
// style data essentially violates style rule immutability because we
// assume that all of the difference calculation will use
// PeekStyleData, which is no longer true with transitions.
// See the FIXME in nsTransitionManager::ConsiderStartingTransition.
unused = getComputedStyle(test, "").color;
test.style.color = "red";
unused = test.offsetWidth;
unused = getComputedStyle(test, "").color;
test.parentNode.style.width = "3em";
unused = test.offsetWidth;
unused = getComputedStyle(test, "").color;
document.documentElement.removeAttribute("class");
}
</script>
<div style="width: 50em">
<span id="test" style="color: green">
This is some text with a transition.
</span>
</div>

View File

@ -0,0 +1,31 @@
<html class="reftest-wait">
<title>Test for CSS transitions and re-wrapping of inlines</title>
<style type="text/css">
#test { -moz-transition: 20ms color linear 0; }
</style>
<script type="text/javascript">
window.onload = run;
function run() {
var test = document.getElementById("test");
var unused = test.offsetWidth;
unused = getComputedStyle(test, "").color;
test.style.color = "green";
unused = test.offsetWidth;
unused = getComputedStyle(test, "").color;
test.parentNode.style.width = "3em";
unused = test.offsetWidth;
unused = getComputedStyle(test, "").color;
setTimeout(step2, 100); // give transition time to run
}
function step2() {
document.documentElement.removeAttribute("class");
}
</script>
<div style="width: 50em">
<span id="test" style="color: red">
This is some text with a transition.
</span>
</div>

View File

@ -53,6 +53,9 @@ include css-mediaqueries/reftest.list
# css namespaces
include css-namespace/reftest.list
# css transitions
include css-transitions/reftest.list
# css values and units
include css-valuesandunits/reftest.list

View File

@ -141,6 +141,7 @@ CPPSRCS = \
nsStyleStruct.cpp \
nsStyleTransformMatrix.cpp \
nsStyleUtil.cpp \
nsTransitionManager.cpp \
$(NULL)
FORCE_STATIC_LIB = 1

View File

@ -58,6 +58,7 @@
#include "nsIFrame.h"
#include "nsContentUtils.h"
#include "nsRuleProcessorData.h"
#include "nsTransitionManager.h"
NS_IMPL_ISUPPORTS1(nsEmptyStyleRule, nsIStyleRule)
@ -113,6 +114,8 @@ nsStyleSet::Init(nsPresContext *aPresContext)
return NS_ERROR_OUT_OF_MEMORY;
}
GatherRuleProcessors(eTransitionSheet);
return NS_OK;
}
@ -192,6 +195,13 @@ nsStyleSet::GatherRuleProcessors(sheetType aType)
//don't regather if this level is disabled
return NS_OK;
}
if (aType == eTransitionSheet) {
// We have no sheet for the transitions level; just a rule
// processor. (XXX: We should probably do this for the other
// non-CSS levels too!)
mRuleProcessors[aType] = PresContext()->TransitionManager();
return NS_OK;
}
if (mSheets[aType].Count()) {
switch (aType) {
case eAgentSheet:
@ -586,6 +596,16 @@ nsStyleSet::FileRules(nsIStyleRuleProcessor::EnumFunc aCollectorFunc,
aRuleWalker->SetLevel(eAgentSheet, PR_TRUE);
AddImportantRules(lastAgentRN, nsnull, aRuleWalker); //agent
#ifdef DEBUG
nsRuleNode *lastImportantRN = aRuleWalker->GetCurrentNode();
#endif
aRuleWalker->SetLevel(eTransitionSheet, PR_FALSE);
(*aCollectorFunc)(mRuleProcessors[eTransitionSheet], aData);
#ifdef DEBUG
AssertNoCSSRules(aRuleWalker->GetCurrentNode(), lastImportantRN);
AssertNoImportantRules(aRuleWalker->GetCurrentNode(), lastImportantRN);
#endif
}
// Enumerate all the rules in a way that doesn't care about the order
@ -623,6 +643,7 @@ nsStyleSet::WalkRuleProcessors(nsIStyleRuleProcessor::EnumFunc aFunc,
(*aFunc)(mRuleProcessors[eStyleAttrSheet], aData);
if (mRuleProcessors[eOverrideSheet])
(*aFunc)(mRuleProcessors[eOverrideSheet], aData);
(*aFunc)(mRuleProcessors[eTransitionSheet], aData);
}
PRBool nsStyleSet::BuildDefaultStyleData(nsPresContext* aPresContext)

View File

@ -196,6 +196,7 @@ class nsStyleSet
eDocSheet, // CSS
eStyleAttrSheet,
eOverrideSheet, // CSS
eTransitionSheet,
eSheetTypeCount
// be sure to keep the number of bits in |mDirty| below and in
// NS_RULE_NODE_LEVEL_MASK updated when changing the number of sheet
@ -351,7 +352,7 @@ class nsStyleSet
unsigned mInShutdown : 1;
unsigned mAuthorStyleDisabled: 1;
unsigned mInReconstruct : 1;
unsigned mDirty : 7; // one dirty bit is used per sheet type
unsigned mDirty : 8; // one dirty bit is used per sheet type
};

View File

@ -1909,6 +1909,14 @@ nsChangeHint nsStyleDisplay::CalcDifference(const nsStyleDisplay& aOther) const
}
}
// Note: Our current behavior for handling changes to transition
// properties is to do nothing. In other words, the transition
// property that matters is what it is when the transition begins, and
// we don't stop a transition later because the transition property
// changed.
// FIXME: Need to test for this and write it in the spec, if it's
// compatible with other browsers. Test for behavior at
// http://dbaron.org/css/test/2009/transitions/dynamic-transition-change
return hint;
}

View File

@ -0,0 +1,899 @@
/* vim: set shiftwidth=2 tabstop=8 autoindent cindent expandtab: */
/* ***** BEGIN LICENSE BLOCK *****
* Version: MPL 1.1/GPL 2.0/LGPL 2.1
*
* The contents of this file are subject to the Mozilla Public License Version
* 1.1 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
* http://www.mozilla.org/MPL/
*
* Software distributed under the License is distributed on an "AS IS" basis,
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
* for the specific language governing rights and limitations under the
* License.
*
* The Original Code is nsTransitionManager.
*
* The Initial Developer of the Original Code is the Mozilla Foundation.
* Portions created by the Initial Developer are Copyright (C) 2009
* the Initial Developer. All Rights Reserved.
*
* Contributor(s):
* L. David Baron <dbaron@dbaron.org>, Mozilla Corporation (original author)
*
* Alternatively, the contents of this file may be used under the terms of
* either the GNU General Public License Version 2 or later (the "GPL"), or
* the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
* in which case the provisions of the GPL or the LGPL are applicable instead
* of those above. If you wish to allow use of your version of this file only
* under the terms of either the GPL or the LGPL, and not to allow others to
* use your version of this file under the terms of the MPL, indicate your
* decision by deleting the provisions above and replace them with the notice
* and other provisions required by the GPL or the LGPL. If you do not delete
* the provisions above, a recipient may use your version of this file under
* the terms of any one of the MPL, the GPL or the LGPL.
*
* ***** END LICENSE BLOCK ***** */
/* Code to start and animate CSS transitions. */
#include "nsTransitionManager.h"
#include "nsIContent.h"
#include "nsStyleContext.h"
#include "nsCSSProps.h"
#include "mozilla/TimeStamp.h"
#include "nsRefreshDriver.h"
#include "nsRuleProcessorData.h"
#include "nsIStyleRule.h"
#include "nsRuleWalker.h"
#include "nsRuleData.h"
#include "nsSMILKeySpline.h"
#include "gfxColor.h"
#include "nsCSSPseudoElements.h"
#include "nsCSSPropertySet.h"
#include "nsStyleAnimation.h"
using mozilla::TimeStamp;
using mozilla::TimeDuration;
/*****************************************************************************
* Per-Element data *
*****************************************************************************/
struct ElementPropertyTransition
{
nsCSSProperty mProperty;
nsStyleCoord mStartValue, mEndValue;
TimeStamp mStartTime; // actual start plus transition delay
// data from the relevant nsTransition
TimeDuration mDuration;
nsSMILKeySpline mTimingFunction;
};
/**
* An ElementTransitionsStyleRule overrides style data with the
* currently-transitioning value for an element that is executing a
* transition. It only matches when styling with animation. When we
* style without animation, we need to not use it so that we can detect
* any new changes; if necessary we restyle immediately afterwards with
* animation.
*/
class ElementTransitionsStyleRule : public nsIStyleRule
{
public:
// nsISupportsImplementation
NS_DECL_ISUPPORTS
// nsIStyleRule implementation
NS_IMETHOD MapRuleInfoInto(nsRuleData* aRuleData);
#ifdef DEBUG
NS_IMETHOD List(FILE* out = stdout, PRInt32 aIndent = 0) const;
#endif
ElementTransitionsStyleRule(ElementTransitions *aOwner,
TimeStamp aRefreshTime)
: mElementTransitions(aOwner)
, mRefreshTime(aRefreshTime)
{}
void Disconnect() { mElementTransitions = nsnull; }
ElementTransitions *ElementData() { return mElementTransitions; }
TimeStamp RefreshTime() { return mRefreshTime; }
private:
ElementTransitions *mElementTransitions;
// The time stamp for which this style rule is valid.
TimeStamp mRefreshTime;
};
/**
* A CoverTransitionStyleRule sets any value for which we're starting a
* transition back to the pre-transition value for the period when we're
* resolving style on its descendants, so that we have the required
* behavior for initiating transitions on such descendants. For more
* detail, see comment below, above "new CoverTransitionStartStyleRule".
*/
class CoverTransitionStartStyleRule : public nsIStyleRule
{
public:
// nsISupportsImplementation
NS_DECL_ISUPPORTS
// nsIStyleRule implementation
NS_IMETHOD MapRuleInfoInto(nsRuleData* aRuleData);
#ifdef DEBUG
NS_IMETHOD List(FILE* out = stdout, PRInt32 aIndent = 0) const;
#endif
NS_HIDDEN_(void) CoverValue(nsCSSProperty aProperty,
nsStyleCoord &aStartValue)
{
CoveredValue v = { aProperty, aStartValue };
mCoveredValues.AppendElement(v);
}
NS_HIDDEN_(void)
FillStyleStruct(void* aStyleStruct, nsRuleData* aRuleData) const;
struct CoveredValue {
nsCSSProperty mProperty;
nsStyleCoord mCoveredValue;
};
private:
nsTArray<CoveredValue> mCoveredValues;
};
struct ElementTransitions : public PRCList
{
ElementTransitions(nsIContent *aElement, nsIAtom *aElementProperty,
nsTransitionManager *aTransitionManager)
: mElement(aElement)
, mElementProperty(aElementProperty)
, mTransitionManager(aTransitionManager)
{
PR_INIT_CLIST(this);
}
~ElementTransitions()
{
DropStyleRule();
PR_REMOVE_LINK(this);
mTransitionManager->TransitionsRemoved();
}
void Destroy()
{
// This will call our destructor.
mElement->DeleteProperty(mElementProperty);
}
void DropStyleRule();
PRBool EnsureStyleRuleFor(TimeStamp aRefreshTime);
// Either zero or one for each CSS property:
nsTArray<ElementPropertyTransition> mPropertyTransitions;
// The style rule for the transitions (which contains the time stamp
// for which it is valid).
nsRefPtr<ElementTransitionsStyleRule> mStyleRule;
nsIContent *mElement;
// the atom we use in mElement's prop table (must be a static atom,
// i.e., in an atom list)
nsIAtom *mElementProperty;
nsTransitionManager *mTransitionManager;
};
static void
ElementTransitionsPropertyDtor(void *aObject,
nsIAtom *aPropertyName,
void *aPropertyValue,
void *aData)
{
ElementTransitions *et = static_cast<ElementTransitions*>(aPropertyValue);
delete et;
}
NS_IMPL_ISUPPORTS1(ElementTransitionsStyleRule, nsIStyleRule)
static void
ElementTransitionsPostResolveCallback(void* aStyleStruct, nsRuleData* aRuleData,
nsIStyleRule* aRule)
{
ElementTransitionsStyleRule *rule =
static_cast<ElementTransitionsStyleRule*>(aRule);
ElementTransitions *et = rule->ElementData();
for (PRUint32 i = 0, i_end = et->mPropertyTransitions.Length();
i < i_end; ++i)
{
const ElementPropertyTransition &pt = et->mPropertyTransitions[i];
if (aRuleData->mSIDs & nsCachedStyleData::GetBitForSID(
nsCSSProps::kSIDTable[pt.mProperty]))
{
double timePortion =
(rule->RefreshTime() - pt.mStartTime).ToSeconds() /
pt.mDuration.ToSeconds();
if (timePortion < 0.0)
timePortion = 0.0; // use start value during transition-delay
if (timePortion > 1.0)
timePortion = 1.0; // we might be behind on flushing
double valuePortion =
pt.mTimingFunction.GetSplineValue(timePortion);
nsStyleCoord value;
#ifdef DEBUG
PRBool ok =
#endif
nsStyleAnimation::Interpolate(pt.mStartValue, pt.mEndValue,
valuePortion, value);
NS_ABORT_IF_FALSE(ok, "could not interpolate values");
#ifdef DEBUG
ok =
#endif
nsStyleAnimation::StoreComputedValue(pt.mProperty,
aRuleData->mPresContext,
aStyleStruct, value);
NS_ABORT_IF_FALSE(ok, "could not store computed value");
}
}
}
NS_IMETHODIMP
ElementTransitionsStyleRule::MapRuleInfoInto(nsRuleData* aRuleData)
{
nsStyleContext *contextParent = aRuleData->mStyleContext->GetParent();
if (contextParent && contextParent->HasPseudoElementData()) {
// Don't apply transitions to things inside of pseudo-elements.
// FIXME: Add tests for this.
return NS_OK;
}
ElementTransitions *et = ElementData();
NS_ENSURE_TRUE(et, NS_OK); // FIXME: Why can this be null?
for (PRUint32 i = 0, i_end = et->mPropertyTransitions.Length();
i < i_end; ++i)
{
ElementPropertyTransition &pt = et->mPropertyTransitions[i];
if (aRuleData->mSIDs & nsCachedStyleData::GetBitForSID(
nsCSSProps::kSIDTable[pt.mProperty]))
{
nsPostResolveCallback prc =
{ &ElementTransitionsPostResolveCallback, this };
aRuleData->mPostResolveCallbacks.AppendElement(prc);
// This really doesn't matter much, since this ought to be
// the only node with the rule, but it's good practice for
// post-resolve callbacks.
aRuleData->mCanStoreInRuleTree = PR_FALSE;
return NS_OK;
}
}
return NS_OK;
}
#ifdef DEBUG
NS_IMETHODIMP
ElementTransitionsStyleRule::List(FILE* out, PRInt32 aIndent) const
{
// WRITE ME?
return NS_OK;
}
#endif
void
ElementTransitions::DropStyleRule()
{
if (mStyleRule) {
mStyleRule->Disconnect();
mStyleRule = nsnull;
}
}
PRBool
ElementTransitions::EnsureStyleRuleFor(TimeStamp aRefreshTime)
{
if (!mStyleRule || mStyleRule->RefreshTime() != aRefreshTime) {
DropStyleRule();
ElementTransitionsStyleRule *newRule =
new ElementTransitionsStyleRule(this, aRefreshTime);
if (!newRule) {
NS_WARNING("out of memory");
return PR_FALSE;
}
mStyleRule = newRule;
}
return PR_TRUE;
}
NS_IMPL_ISUPPORTS1(CoverTransitionStartStyleRule, nsIStyleRule)
static void
CoverTransitionStartPostResolveCallback(void* aStyleStruct,
nsRuleData* aRuleData,
nsIStyleRule* aRule)
{
CoverTransitionStartStyleRule* coverRule =
static_cast<CoverTransitionStartStyleRule*>(aRule);
coverRule->FillStyleStruct(aStyleStruct, aRuleData);
}
NS_IMETHODIMP
CoverTransitionStartStyleRule::MapRuleInfoInto(nsRuleData* aRuleData)
{
for (PRUint32 i = 0, i_end = mCoveredValues.Length(); i < i_end; ++i) {
CoveredValue &cv = mCoveredValues[i];
if (aRuleData->mSIDs & nsCachedStyleData::GetBitForSID(
nsCSSProps::kSIDTable[cv.mProperty]))
{
nsPostResolveCallback prc =
{ &CoverTransitionStartPostResolveCallback, this };
aRuleData->mPostResolveCallbacks.AppendElement(prc);
// This really doesn't matter much, since this ought to be
// the only node with the rule, but it's good practice for
// post-resolve callbacks.
aRuleData->mCanStoreInRuleTree = PR_FALSE;
return NS_OK;
}
}
return NS_OK;
}
#ifdef DEBUG
NS_IMETHODIMP
CoverTransitionStartStyleRule::List(FILE* out, PRInt32 aIndent) const
{
// WRITE ME?
return NS_OK;
}
#endif
void
CoverTransitionStartStyleRule::FillStyleStruct(void* aStyleStruct,
nsRuleData* aRuleData) const
{
for (PRUint32 i = 0, i_end = mCoveredValues.Length(); i < i_end; ++i) {
const CoveredValue &cv = mCoveredValues[i];
if (aRuleData->mSIDs & nsCachedStyleData::GetBitForSID(
nsCSSProps::kSIDTable[cv.mProperty]))
{
#ifdef DEBUG
PRBool ok =
#endif
nsStyleAnimation::StoreComputedValue(cv.mProperty,
aRuleData->mPresContext,
aStyleStruct, cv.mCoveredValue);
NS_ABORT_IF_FALSE(ok, "could not store computed value");
}
}
}
/*****************************************************************************
* nsTransitionManager *
*****************************************************************************/
nsTransitionManager::nsTransitionManager(nsPresContext *aPresContext)
: mPresContext(aPresContext)
{
PR_INIT_CLIST(&mElementTransitions);
}
nsTransitionManager::~nsTransitionManager()
{
// Content nodes might outlive the transition manager.
while (!PR_CLIST_IS_EMPTY(&mElementTransitions)) {
ElementTransitions *head = static_cast<ElementTransitions*>(
PR_LIST_HEAD(&mElementTransitions));
head->Destroy();
}
}
already_AddRefed<nsIStyleRule>
nsTransitionManager::StyleContextChanged(nsIContent *aElement,
nsStyleContext *aOldStyleContext,
nsStyleContext *aNewStyleContext)
{
NS_PRECONDITION(aOldStyleContext->GetPseudoType() ==
aNewStyleContext->GetPseudoType(),
"pseudo type mismatch");
// If we were called from ReParentStyleContext, this assertion would
// actually fire. If we need to be called from there, we can probably
// just remove it; the condition probably isn't critical, although
// it's worth thinking about some more.
NS_PRECONDITION(aOldStyleContext->HasPseudoElementData() ==
aNewStyleContext->HasPseudoElementData(),
"pseudo type mismatch");
// Return sooner (before the startedAny check below) for the most
// common case: no transitions specified.
const nsStyleDisplay *disp = aNewStyleContext->GetStyleDisplay();
if (disp->mTransitionPropertyCount == 1 &&
disp->mTransitions[0].GetDelay() == 0.0f &&
disp->mTransitions[0].GetDuration() == 0.0f) {
return nsnull;
}
if (aNewStyleContext->PresContext()->IsProcessingAnimationStyleChange()) {
return nsnull;
}
nsIAtom *pseudo = aNewStyleContext->GetPseudoType();
if (pseudo && (pseudo != nsCSSPseudoElements::before &&
pseudo != nsCSSPseudoElements::after)) {
return nsnull;
}
if (aNewStyleContext->GetParent() &&
aNewStyleContext->GetParent()->HasPseudoElementData()) {
// Ignore transitions on things that inherit properties from
// pseudo-elements.
// FIXME: Add tests for this.
return nsnull;
}
// FIXME: When we have multiple continuations, we actually repeat this
// for each one, and if we have transitions we create separate cover
// rules for each one. However, since we're attaching the transition
// data to the element, during the animation we create the same style
// rule, so it's not too horrible.
// Per http://lists.w3.org/Archives/Public/www-style/2009Aug/0109.html
// I'll consider only the transitions from the number of items in
// 'transition-property' on down, and later ones will override earlier
// ones (tracked using |whichStarted|).
PRBool startedAny = PR_FALSE;
nsCSSPropertySet whichStarted;
ElementTransitions *et = nsnull;
for (PRUint32 i = disp->mTransitionPropertyCount; i-- != 0; ) {
const nsTransition& t = disp->mTransitions[i];
// Check delay and duration first, since they default to zero, and
// when they're both zero, we can ignore the transition.
if (t.GetDelay() != 0.0f || t.GetDuration() != 0.0f) {
et = GetElementTransitions(aElement,
aNewStyleContext->GetPseudoType(),
PR_FALSE);
// We might have something to transition. See if any of the
// properties in question changed and are animatable.
nsCSSProperty property = t.GetProperty();
if (property == eCSSPropertyExtra_no_properties ||
property == eCSSProperty_UNKNOWN) {
// Nothing to do, but need to exclude this from cases below.
} else if (property == eCSSPropertyExtra_all_properties) {
for (nsCSSProperty p = nsCSSProperty(0);
p < eCSSProperty_COUNT_no_shorthands;
p = nsCSSProperty(p + 1)) {
ConsiderStartingTransition(p, t, aElement, et,
aOldStyleContext, aNewStyleContext,
&startedAny, &whichStarted);
}
} else if (nsCSSProps::IsShorthand(property)) {
CSSPROPS_FOR_SHORTHAND_SUBPROPERTIES(subprop, property) {
ConsiderStartingTransition(*subprop, t, aElement, et,
aOldStyleContext, aNewStyleContext,
&startedAny, &whichStarted);
}
} else {
ConsiderStartingTransition(property, t, aElement, et,
aOldStyleContext, aNewStyleContext,
&startedAny, &whichStarted);
}
}
}
if (!startedAny) {
return nsnull;
}
NS_ABORT_IF_FALSE(et, "must have element transitions if we started "
"any transitions");
// In the CSS working group discussion (2009 Jul 15 telecon,
// http://www.w3.org/mid/4A5E1470.4030904@inkedblade.net ) of
// http://lists.w3.org/Archives/Public/www-style/2009Jun/0121.html ,
// the working group decided that a transition property on an
// element should not cause any transitions if the property change
// is itself inheriting a value that is transitioning on an
// ancestor. So, to get the correct behavior, we continue the
// restyle that caused this transition using a "covering" rule that
// covers up any changes on which we started transitions, so that
// descendants don't start their own transitions. (In the case of
// negative transition delay, this covering rule produces different
// results than applying the transition rule immediately would).
// Our caller is responsible for restyling again using this covering
// rule.
nsRefPtr<CoverTransitionStartStyleRule> coverRule =
new CoverTransitionStartStyleRule;
if (!coverRule) {
NS_WARNING("out of memory");
return nsnull;
}
nsTArray<ElementPropertyTransition> &pts = et->mPropertyTransitions;
for (PRUint32 i = 0, i_end = pts.Length(); i < i_end; ++i) {
ElementPropertyTransition &pt = pts[i];
if (whichStarted.HasProperty(pt.mProperty)) {
coverRule->CoverValue(pt.mProperty, pt.mStartValue);
}
}
return already_AddRefed<nsIStyleRule>(
static_cast<nsIStyleRule*>(coverRule.forget().get()));
}
void
nsTransitionManager::ConsiderStartingTransition(nsCSSProperty aProperty,
const nsTransition& aTransition,
nsIContent *aElement,
ElementTransitions *&aElementTransitions,
nsStyleContext *aOldStyleContext,
nsStyleContext *aNewStyleContext,
PRBool *aStartedAny,
nsCSSPropertySet *aWhichStarted)
{
// IsShorthand itself will assert if aProperty is not a property.
NS_ABORT_IF_FALSE(!nsCSSProps::IsShorthand(aProperty),
"property out of range");
if (aWhichStarted->HasProperty(aProperty)) {
// A later item in transition-property already started a
// transition for this property, so we ignore this one.
// See comment above and
// http://lists.w3.org/Archives/Public/www-style/2009Aug/0109.html .
return;
}
if (nsCSSProps::kAnimTypeTable[aProperty] == eStyleAnimType_None) {
return;
}
ElementPropertyTransition pt;
nsStyleCoord dummyValue;
// FIXME: This call on the old style context gets incorrect style data
// since we don't quite enforce style rule immutability: we didn't
// need to worry about callers calling GetStyleData rather than
// PeekStyleData after a style rule becomes "old" before transitions
// existed.
PRBool shouldAnimate =
nsStyleAnimation::ExtractComputedValue(aProperty, aOldStyleContext,
pt.mStartValue) &&
nsStyleAnimation::ExtractComputedValue(aProperty, aNewStyleContext,
pt.mEndValue) &&
pt.mStartValue != pt.mEndValue &&
// Check that we can interpolate between these values
// (If this is ever a performance problem, we could add a
// CanInterpolate method, but it seems fine for now.)
nsStyleAnimation::Interpolate(pt.mStartValue, pt.mEndValue, 0.5,
dummyValue);
PRUint32 currentIndex = nsTArray<ElementPropertyTransition>::NoIndex;
if (aElementTransitions) {
nsTArray<ElementPropertyTransition> &pts =
aElementTransitions->mPropertyTransitions;
for (PRUint32 i = 0, i_end = pts.Length(); i < i_end; ++i) {
if (pts[i].mProperty == aProperty) {
currentIndex = i;
break;
}
}
}
nsPresContext *presContext = aNewStyleContext->PresContext();
if (!shouldAnimate) {
if (currentIndex != nsTArray<ElementPropertyTransition>::NoIndex) {
// We're in the middle of a transition, but just got a
// non-transition style change changing to exactly the
// current in-progress value. (This is quite easy to cause
// using 'transition-delay'.)
nsTArray<ElementPropertyTransition> &pts =
aElementTransitions->mPropertyTransitions;
pts.RemoveElementAt(currentIndex);
if (pts.IsEmpty()) {
aElementTransitions->Destroy();
// |aElementTransitions| is now a dangling pointer!
aElementTransitions = nsnull;
}
presContext->PresShell()->RestyleForAnimation(aElement);
}
return;
}
// When we interrupt a running transition, we want to reduce the
// duration of the new transition *if* the new transition would have
// been longer had it started from the endpoint of the currently
// running transition.
double durationFraction = 1.0;
// We need to check two things if we have a currently running
// transition for this property: see durationFraction comment above
// and the endpoint check below.
if (currentIndex != nsTArray<ElementPropertyTransition>::NoIndex) {
const nsStyleCoord &endVal =
aElementTransitions->mPropertyTransitions[currentIndex].mEndValue;
if (endVal == pt.mEndValue) {
// If we got a style change that changed the value to the endpoint
// of the currently running transition, we don't want to interrupt
// its timing function.
// But don't forget to restyle with animation so we show the
// current transition.
presContext->PresShell()->RestyleForAnimation(aElement);
return;
}
double fullDistance, remainingDistance;
#ifdef DEBUG
PRBool ok =
#endif
nsStyleAnimation::ComputeDistance(pt.mStartValue, pt.mEndValue,
fullDistance);
NS_ABORT_IF_FALSE(ok, "could not compute distance");
NS_ABORT_IF_FALSE(fullDistance >= 0.0, "distance must be positive");
if (nsStyleAnimation::ComputeDistance(endVal, pt.mEndValue,
remainingDistance)) {
NS_ABORT_IF_FALSE(remainingDistance >= 0.0, "distance must be positive");
durationFraction = fullDistance / remainingDistance;
if (durationFraction > 1.0) {
durationFraction = 1.0;
}
}
}
nsRefreshDriver *rd = presContext->RefreshDriver();
pt.mProperty = aProperty;
float delay = aTransition.GetDelay();
float duration = aTransition.GetDuration();
if (durationFraction != 1.0) {
// Negative delays are essentially part of the transition
// function, so reduce them along with the duration, but don't
// reduce positive delays. (See comment above about
// durationFraction.)
if (delay < 0.0f)
delay *= durationFraction;
duration *= durationFraction;
}
pt.mStartTime = rd->MostRecentRefresh() +
TimeDuration::FromMilliseconds(delay);
pt.mDuration = TimeDuration::FromMilliseconds(duration);
const nsTimingFunction &tf = aTransition.GetTimingFunction();
pt.mTimingFunction.Init(tf.mX1, tf.mY1, tf.mX2, tf.mY2);
if (!aElementTransitions) {
aElementTransitions =
GetElementTransitions(aElement, aNewStyleContext->GetPseudoType(),
PR_TRUE);
if (!aElementTransitions) {
NS_WARNING("allocating ElementTransitions failed");
return;
}
}
nsTArray<ElementPropertyTransition> &pts =
aElementTransitions->mPropertyTransitions;
#ifdef DEBUG
for (PRUint32 i = 0, i_end = pts.Length(); i < i_end; ++i) {
NS_ABORT_IF_FALSE(i == currentIndex ||
pts[i].mProperty != aProperty,
"duplicate transitions for property");
}
#endif
if (currentIndex != nsTArray<ElementPropertyTransition>::NoIndex) {
pts[currentIndex] = pt;
} else {
if (!pts.AppendElement(pt)) {
NS_WARNING("out of memory");
return;
}
}
presContext->PresShell()->RestyleForAnimation(aElement);
*aStartedAny = PR_TRUE;
aWhichStarted->AddProperty(aProperty);
}
ElementTransitions*
nsTransitionManager::GetElementTransitions(nsIContent *aElement,
nsIAtom *aPseudo,
PRBool aCreateIfNeeded)
{
nsIAtom *propName;
if (aPseudo == nsCSSPseudoElements::before) {
propName = nsGkAtoms::transitionsOfBeforeProperty;
} else if (aPseudo == nsCSSPseudoElements::after) {
propName = nsGkAtoms::transitionsOfAfterProperty;
} else {
NS_ASSERTION(!aPseudo || !aCreateIfNeeded,
"should never try to create transitions for pseudo "
"other than :before or :after");
propName = nsGkAtoms::transitionsProperty;
}
ElementTransitions *et = static_cast<ElementTransitions*>(
aElement->GetProperty(propName));
if (!et && aCreateIfNeeded) {
// FIXME: Consider arena-allocating?
et = new ElementTransitions(aElement, propName, this);
if (!et) {
NS_WARNING("out of memory");
return nsnull;
}
nsresult rv = aElement->SetProperty(propName, et,
ElementTransitionsPropertyDtor, nsnull);
if (NS_FAILED(rv)) {
NS_WARNING("SetProperty failed");
delete et;
return nsnull;
}
AddElementTransitions(et);
}
return et;
}
void
nsTransitionManager::AddElementTransitions(ElementTransitions* aElementTransitions)
{
if (PR_CLIST_IS_EMPTY(&mElementTransitions)) {
// We need to observe the refresh driver.
nsRefreshDriver *rd = mPresContext->RefreshDriver();
rd->AddRefreshObserver(this, Flush_Style);
}
PR_INSERT_BEFORE(aElementTransitions, &mElementTransitions);
}
/*
* nsISupports implementation
*/
NS_IMPL_ADDREF_USING_AGGREGATOR(nsTransitionManager, mPresContext)
NS_IMPL_RELEASE_USING_AGGREGATOR(nsTransitionManager, mPresContext)
NS_IMPL_QUERY_INTERFACE1(nsTransitionManager, nsIStyleRuleProcessor)
/*
* nsIStyleRuleProcessor implementation
*/
nsresult
nsTransitionManager::WalkTransitionRule(RuleProcessorData* aData,
nsIAtom *aPseudo)
{
if (!aData->mPresContext->IsProcessingAnimationStyleChange()) {
// If we're processing a normal style change rather than one from
// animation, don't add the transition rule. This allows us to
// compute the new style value rather than having the transition
// override it, so that we can start transitioning differently.
// In most cases, we need to immediately restyle with animation
// after doing this. However, ConsiderStartingTransition takes care
// of that for us.
return NS_OK;
}
ElementTransitions *et =
GetElementTransitions(aData->mContent, aPseudo, PR_FALSE);
if (!et) {
return NS_OK;
}
if (!et->EnsureStyleRuleFor(
aData->mPresContext->RefreshDriver()->MostRecentRefresh())) {
return NS_ERROR_OUT_OF_MEMORY;
}
aData->mRuleWalker->Forward(et->mStyleRule);
return NS_OK;
}
NS_IMETHODIMP
nsTransitionManager::RulesMatching(ElementRuleProcessorData* aData)
{
NS_ABORT_IF_FALSE(aData->mPresContext == mPresContext,
"pres context mismatch");
return WalkTransitionRule(aData, nsnull);
}
NS_IMETHODIMP
nsTransitionManager::RulesMatching(PseudoRuleProcessorData* aData)
{
NS_ABORT_IF_FALSE(aData->mPresContext == mPresContext,
"pres context mismatch");
// Note: If we're the only thing keeping a pseudo-element frame alive
// (per ProbePseudoStyleContext), we still want to keep it alive, so
// this is ok.
return WalkTransitionRule(aData, aData->mPseudoTag);
}
NS_IMETHODIMP
nsTransitionManager::HasStateDependentStyle(StateRuleProcessorData* aData,
nsReStyleHint* aResult)
{
*aResult = nsReStyleHint(0);
return NS_OK;
}
NS_IMETHODIMP
nsTransitionManager::HasAttributeDependentStyle(AttributeRuleProcessorData* aData,
nsReStyleHint* aResult)
{
*aResult = nsReStyleHint(0);
return NS_OK;
}
NS_IMETHODIMP
nsTransitionManager::MediumFeaturesChanged(nsPresContext* aPresContext,
PRBool* aRulesChanged)
{
*aRulesChanged = PR_FALSE;
return NS_OK;
}
/* virtual */ void
nsTransitionManager::WillRefresh(mozilla::TimeStamp aTime)
{
// Trim transitions that have completed, and post restyle events for
// frames that are still transitioning.
{
PRCList *next = PR_LIST_HEAD(&mElementTransitions);
while (next != &mElementTransitions) {
ElementTransitions *et = static_cast<ElementTransitions*>(next);
next = PR_NEXT_LINK(next);
NS_ABORT_IF_FALSE(et->mElement->GetCurrentDoc() ==
mPresContext->Document(),
"nsGenericElement::UnbindFromTree should have "
"destroyed the element transitions object");
PRUint32 i = et->mPropertyTransitions.Length();
NS_ABORT_IF_FALSE(i != 0, "empty transitions list?");
do {
--i;
ElementPropertyTransition &pt = et->mPropertyTransitions[i];
if (pt.mStartTime + pt.mDuration <= aTime) {
// This transition has completed.
et->mPropertyTransitions.RemoveElementAt(i);
}
} while (i != 0);
// We need to restyle even if the transition rule no longer
// applies (in which case we just made it not apply).
mPresContext->PresShell()->RestyleForAnimation(et->mElement);
if (et->mPropertyTransitions.IsEmpty()) {
et->Destroy();
// |et| is now a dangling pointer!
et = nsnull;
}
}
}
// We might have removed transitions above.
TransitionsRemoved();
}
void
nsTransitionManager::TransitionsRemoved()
{
// If we have no transitions left, remove ourselves from the refresh
// driver.
if (PR_CLIST_IS_EMPTY(&mElementTransitions)) {
mPresContext->RefreshDriver()->RemoveRefreshObserver(this, Flush_Style);
}
}

View File

@ -0,0 +1,121 @@
/* vim: set shiftwidth=2 tabstop=8 autoindent cindent expandtab: */
/* ***** BEGIN LICENSE BLOCK *****
* Version: MPL 1.1/GPL 2.0/LGPL 2.1
*
* The contents of this file are subject to the Mozilla Public License Version
* 1.1 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
* http://www.mozilla.org/MPL/
*
* Software distributed under the License is distributed on an "AS IS" basis,
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
* for the specific language governing rights and limitations under the
* License.
*
* The Original Code is nsTransitionManager.
*
* The Initial Developer of the Original Code is the Mozilla Foundation.
* Portions created by the Initial Developer are Copyright (C) 2009
* the Initial Developer. All Rights Reserved.
*
* Contributor(s):
* L. David Baron <dbaron@dbaron.org>, Mozilla Corporation (original author)
*
* Alternatively, the contents of this file may be used under the terms of
* either the GNU General Public License Version 2 or later (the "GPL"), or
* the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
* in which case the provisions of the GPL or the LGPL are applicable instead
* of those above. If you wish to allow use of your version of this file only
* under the terms of either the GPL or the LGPL, and not to allow others to
* use your version of this file under the terms of the MPL, indicate your
* decision by deleting the provisions above and replace them with the notice
* and other provisions required by the GPL or the LGPL. If you do not delete
* the provisions above, a recipient may use your version of this file under
* the terms of any one of the MPL, the GPL or the LGPL.
*
* ***** END LICENSE BLOCK ***** */
/* Code to start and animate CSS transitions. */
#ifndef nsTransitionManager_h_
#define nsTransitionManager_h_
#include "prclist.h"
#include "nsCSSProperty.h"
#include "nsIStyleRuleProcessor.h"
#include "nsRefreshDriver.h"
class nsStyleContext;
class nsPresContext;
class nsCSSPropertySet;
struct nsTransition;
struct ElementTransitions;
/**
* Must be created only as a sub-object of an nsPresContext (since its
* reference counting methods assume that).
*/
class nsTransitionManager : public nsIStyleRuleProcessor,
public nsARefreshObserver {
public:
nsTransitionManager(nsPresContext *aPresContext);
~nsTransitionManager();
/**
* StyleContextChanged
*
* To be called from nsFrameManager::ReResolveStyleContext when the
* style of an element has changed, to initiate transitions from that
* style change.
*
* It may return a "cover rule" (see CoverTransitionStartStyleRule) to
* cover up some of the changes for the duration of the restyling of
* descendants. If it does, this function will take care of causing
* the necessary restyle afterwards, but the caller must restyle the
* element *again* with the original sequence of rules plus the
* returned cover rule as the most specific rule.
*/
already_AddRefed<nsIStyleRule>
StyleContextChanged(nsIContent *aElement,
nsStyleContext *aOldStyleContext,
nsStyleContext *aNewStyleContext);
// nsISupports
NS_DECL_ISUPPORTS_INHERITED
// nsIStyleRuleProcessor
NS_IMETHOD RulesMatching(ElementRuleProcessorData* aData);
NS_IMETHOD RulesMatching(PseudoRuleProcessorData* aData);
NS_IMETHOD HasStateDependentStyle(StateRuleProcessorData* aData,
nsReStyleHint* aResult);
NS_IMETHOD HasAttributeDependentStyle(AttributeRuleProcessorData* aData,
nsReStyleHint* aResult);
NS_IMETHOD MediumFeaturesChanged(nsPresContext* aPresContext,
PRBool* aRulesChanged);
// nsARefreshObserver
virtual void WillRefresh(mozilla::TimeStamp aTime);
private:
friend class ElementTransitions; // for TransitionsRemoved
void ConsiderStartingTransition(nsCSSProperty aProperty,
const nsTransition& aTransition,
nsIContent *aElement,
ElementTransitions *&aElementTransitions,
nsStyleContext *aOldStyleContext,
nsStyleContext *aNewStyleContext,
PRBool *aStartedAny,
nsCSSPropertySet *aWhichStarted);
ElementTransitions* GetElementTransitions(nsIContent *aElement,
nsIAtom *aPseudo,
PRBool aCreateIfNeeded);
void AddElementTransitions(ElementTransitions* aElementTransitions);
void TransitionsRemoved();
nsresult WalkTransitionRule(RuleProcessorData* aData, nsIAtom *aPseudo);
PRCList mElementTransitions;
nsPresContext *mPresContext;
};
#endif /* !defined(nsTransitionManager_h_) */

View File

@ -136,6 +136,7 @@ _TEST_FILES = test_acid3_test46.html \
test_system_font_serialization.html \
test_transitions_computed_values.html \
test_transitions_computed_value_combinations.html \
test_transitions.html \
test_units_angle.html \
test_units_frequency.html \
test_units_length.html \

View File

@ -0,0 +1,626 @@
<!DOCTYPE HTML>
<html>
<!--
https://bugzilla.mozilla.org/show_bug.cgi?id=435441
-->
<head>
<title>Test for Bug 435441</title>
<script type="application/javascript" src="/MochiKit/packed.js"></script>
<script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
<style type="text/css">
#display p { margin-top: 0; margin-bottom: 0; }
</style>
</head>
<body>
<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=435441">Mozilla Bug 435441</a>
<div id="display">
</div>
<pre id="test">
<script type="application/javascript">
/** Test for Bug 435441 **/
function px_to_num(str)
{
return Number(String(str).match(/^([\d.]+)px$/)[1]);
}
// Run tests simultaneously so we don't have to take up too much time.
SimpleTest.waitForExplicitFinish();
var gTestsRunning = 0;
function TestStarted() { ++gTestsRunning; }
function TestFinished() { if (--gTestsRunning == 0) SimpleTest.finish(); }
// An array of arrays of functions to be called at the outer index number
// of seconds after the present.
var gFutureCalls = [];
function add_future_call(index, func)
{
if (!(index in gFutureCalls)) {
gFutureCalls[index] = [];
}
gFutureCalls[index].push(func);
TestStarted();
}
var gStartTime1, gStartTime2;
var gCurrentTime;
function process_future_calls(index)
{
var calls = gFutureCalls[index];
if (!calls)
return;
gCurrentTime = Date.now();
for (var i = 0; i < calls.length; ++i) {
calls[i]();
TestFinished();
}
}
function bezier(x1, y1, x2, y2) {
// Cubic bezier with control points (0, 0), (x1, y1), (x2, y2), and (1, 1).
function x_for_t(t) {
var omt = 1-t;
return 3 * omt * omt * t * x1 + 3 * omt * t * t * x2 + t * t * t;
}
function y_for_t(t) {
var omt = 1-t;
return 3 * omt * omt * t * y1 + 3 * omt * t * t * y2 + t * t * t;
}
function t_for_x(x) {
// Binary subdivision.
var mint = 0, maxt = 1;
for (var i = 0; i < 30; ++i) {
var guesst = (mint + maxt) / 2;
var guessx = x_for_t(guesst);
if (x < guessx)
maxt = guesst;
else
mint = guesst;
}
return (mint + maxt) / 2;
}
return function bezier_closure(x) {
if (x == 0) return 0;
if (x == 1) return 1;
return y_for_t(t_for_x(x));
}
}
var timingFunctions = {
// a map from the value of 'transition-timing-function' to an array of
// the portions this function yields at 0 (always 0), 1/4, 1/2, and
// 3/4 and all (always 1) of the way through the time of the
// transition. Each portion is represented as a value and an
// acceptable error tolerance (based on a time error of 1%) for that
// value.
// ease
"ease": bezier(0.25, 0.1, 0.25, 1),
"cubic-bezier(0.25, 0.1, 0.25, 1.0)": bezier(0.25, 0.1, 0.25, 1),
// linear and various synonyms for it
"linear": function(x) { return x; },
"cubic-bezier(0.0, 0.0, 1.0, 1.0)": function(x) { return x; },
"cubic-bezier(0, 0, 1, 1)": function(x) { return x; },
"cubic-bezier(0, 0, 0, 0.0)": function(x) { return x; },
"cubic-bezier(1.0, 1, 0, 0)": function(x) { return x; },
// ease-in
"ease-in": bezier(0.42, 0, 1, 1),
"cubic-bezier(0.42, 0, 1.0, 1.0)": bezier(0.42, 0, 1, 1),
// ease-out
"ease-out": bezier(0, 0, 0.58, 1),
"cubic-bezier(0, 0, 0.58, 1.0)": bezier(0, 0, 0.58, 1),
// ease-in-out
"ease-in-out": bezier(0.42, 0, 0.58, 1),
"cubic-bezier(0.42, 0, 0.58, 1.0)": bezier(0.42, 0, 0.58, 1),
// other cubic-bezier values
"cubic-bezier(0.4, 0.1, 0.7, 0.95)": bezier(0.4, 0.1, 0.7, 0.95),
"cubic-bezier(1, 0, 0, 1)": bezier(1, 0, 0, 1),
"cubic-bezier(0, 1, 1, 0)": bezier(0, 1, 1, 0),
};
var div = document.getElementById("display");
// Set up all the elements on which we are going to start transitions.
// Test all timing functions using a set of 8-second transitions, which
// we check at times 0, 2s, 4s, 6s, and 8s.
var tftests = [];
for (var tf in timingFunctions) {
var p = document.createElement("p");
var t = document.createTextNode("transition-timing-function: " + tf);
p.appendChild(t);
p.style.textIndent = "0px";
p.style.MozTransition = "8s text-indent linear";
p.style.MozTransitionTimingFunction = tf;
div.appendChild(p);
is(getComputedStyle(p, "").textIndent, "0px",
"should be zero before changing value");
tftests.push([ p, tf ]);
}
// Check that the timing function continues even when we restyle in the
// middle.
var interrupt_tests = [];
for (var itime = 2; itime < 8; itime += 2) {
var p = document.createElement("p");
var t = document.createTextNode("interrupt at " + itime + "s");
p.appendChild(t);
p.style.textIndent = "0px";
p.style.MozTransition = "8s text-indent cubic-bezier(0, 1, 1, 0)";
div.appendChild(p);
is(getComputedStyle(p, "").textIndent, "0px",
"should be zero before changing value");
interrupt_tests.push([ p, itime ]);
}
setTimeout("interrupt_tests[0][0].style.color = 'blue';" +
"check_interrupt_tests()", 2000);
setTimeout("interrupt_tests[1][0].style.color = 'blue';" +
"check_interrupt_tests()", 4000);
setTimeout("interrupt_tests[2][0].style.color = 'blue';" +
"check_interrupt_tests()", 6000);
// Test transition-delay values of -4s through 4s on a 4s transition
// with 'ease-out' timing function.
var delay_tests = {};
for (var d = -4; d <= 4; ++d) {
var p = document.createElement("p");
var delay = d + "s";
var t = document.createTextNode("transition-delay: " + delay);
p.appendChild(t);
p.style.marginLeft = "0px";
p.style.MozTransition = "4s margin-left ease-out " + delay;
div.appendChild(p);
is(getComputedStyle(p, "").marginLeft, "0px",
"should be zero before changing value");
delay_tests[d] = p;
}
// Test that changing the value on an already-running transition to the
// value it currently happens to have resets the transition.
var p = document.createElement("p");
var t = document.createTextNode("transition-delay reset to starting point");
p.appendChild(t);
p.style.marginLeft = "0px";
p.style.MozTransition = "4s margin-left ease-out 4s";
div.appendChild(p);
is(getComputedStyle(p, "").marginLeft, "0px",
"should be zero before changing value");
var reset_test = p;
// Test that transitions on descendants do not trigger when the
// inherited value is itself transitioning. In other words, when
// ancestor and descendant both have a transition for the same property,
// and the descendant inherits the property from the ancestor, the
// descendant's transition is ignored (as part of the idea of not
// starting transitions on changes that result from animation).
// See http://lists.w3.org/Archives/Public/www-style/2009Jun/0121.html
// and http://lists.w3.org/Archives/Public/www-style/2009Jul/0050.html
var descendant_tests = [
{ parent_transition: "",
child_transition: "4s text-indent" },
{ parent_transition: "4s text-indent",
child_transition: "" },
{ parent_transition: "4s text-indent",
child_transition: "16s text-indent" },
{ parent_transition: "4s text-indent",
child_transition: "1s text-indent" },
{ parent_transition: "8s letter-spacing",
child_transition: "4s text-indent" },
{ parent_transition: "4s text-indent",
child_transition: "8s letter-spacing" },
{ parent_transition: "4s text-indent",
child_transition: "8s all" },
{ parent_transition: "8s text-indent",
child_transition: "4s all" },
// examples with positive and negative delay
{ parent_transition: "4s text-indent 1s",
child_transition: "8s text-indent" },
{ parent_transition: "4s text-indent -1s",
child_transition: "8s text-indent" }
];
for (var i in descendant_tests) {
var test = descendant_tests[i];
test.parentNode = document.createElement("div");
test.childNode = document.createElement("p");
test.parentNode.appendChild(test.childNode);
test.childNode.appendChild(document.createTextNode(
"parent with \"" + test.parent_transition + "\" and " +
"child with \"" + test.child_transition + "\""));
test.parentNode.style.MozTransition = test.parent_transition;
test.childNode.style.MozTransition = test.child_transition;
test.parentNode.style.textIndent = "50px"; // transition from 50 to 150
test.parentNode.style.letterSpacing = "10px"; // transition from 10 to 5
div.appendChild(test.parentNode);
var parentCS = getComputedStyle(test.parentNode, "");
var childCS = getComputedStyle(test.childNode, "");
is(parentCS.textIndent, "50px",
"parent text-indent should be 50px before changing");
is(parentCS.letterSpacing, "10px",
"parent letter-spacing should be 10px before changing");
is(childCS.textIndent, "50px",
"child text-indent should be 50px before changing");
is(childCS.letterSpacing, "10px",
"child letter-spacing should be 10px before changing");
test.childCS = childCS;
}
// For all of these transitions, the transition for margin-left should
// have a duration of 8s, and the default timing function (ease) and
// delay (0).
// This is because we're implementing the proposal in
// http://lists.w3.org/Archives/Public/www-style/2009Aug/0109.html
var number_tests = [
{ style: "-moz-transition: 4s margin, 8s margin-left" },
{ style: "-moz-transition: 4s margin-left, 8s margin" },
{ style: "-moz-transition-property: margin-left; " +
"-moz-transition-duration: 8s, 2s" },
{ style: "-moz-transition-property: margin-left, margin-left; " +
"-moz-transition-duration: 2s, 8s" },
{ style: "-moz-transition-property: margin-left, margin-left, margin-left; " +
"-moz-transition-duration: 8s, 2s" },
{ style: "-moz-transition-property: margin-left; " +
"-moz-transition-duration: 8s, 16s" },
{ style: "-moz-transition-property: margin-left, margin-left; " +
"-moz-transition-duration: 16s, 8s" },
{ style: "-moz-transition-property: margin-left, margin-left, margin-left; " +
"-moz-transition-duration: 8s, 16s" },
{ style: "-moz-transition-property: text-indent,word-spacing,margin-left; " +
"-moz-transition-duration: 8s; " +
"-moz-transition-delay: 0, 8s" },
{ style: "-moz-transition-property: text-indent,word-spacing,margin-left; " +
"-moz-transition-duration: 8s, 16s; " +
"-moz-transition-delay: 8s, 8s, 0, 8s, 8s, 8s" },
];
for (var i in number_tests) {
var test = number_tests[i];
var p = document.createElement("p");
p.setAttribute("style", test.style);
var t = document.createTextNode(test.style);
p.appendChild(t);
p.style.marginLeft = "100px";
div.appendChild(p);
is(getComputedStyle(p, "").marginLeft, "100px",
"should be 100px before changing value");
test.node = p;
}
// Test transitions that are also from-display:none, to-display:none, and
// display:none throughout.
var from_none_test, to_none_test, always_none_test;
function make_display_test(initially_none, text)
{
var p = document.createElement("p");
p.appendChild(document.createTextNode(text));
p.style.textIndent = "0px";
p.style.MozTransition = "8s text-indent ease-in-out";
if (initially_none)
p.style.display = "none";
div.appendChild(p);
return p;
}
from_none_test = make_display_test(true, "transition from display:none");
to_none_test = make_display_test(false, "transition to display:none");
always_none_test = make_display_test(true, "transition always display:none");
var display_tests = [ from_none_test, to_none_test, always_none_test ];
// FIXME: Test a transition that reverses partway through.
// flush style changes
var x = getComputedStyle(div, "").color;
// Start our timer as close as possible to when we start the first
// transition.
// Do not use setInterval because once it gets off in time, it stays off.
for (var i = 1; i <= 8; ++i) {
setTimeout(process_future_calls, i * 1000, i);
}
gStartTime1 = Date.now(); // set before any transitions have started
// Start all the transitions.
for (var test in tftests) {
var p = tftests[test][0];
p.style.textIndent = "100px";
}
for (var test in interrupt_tests) {
var p = interrupt_tests[test][0];
p.style.textIndent = "100px";
}
for (var d in delay_tests) {
var p = delay_tests[d];
p.style.marginLeft = "100px";
}
reset_test.style.marginLeft = "100px";
for (var i in descendant_tests) {
var test = descendant_tests[i];
test.parentNode.style.textIndent = "150px";
test.parentNode.style.letterSpacing = "5px";
}
for (var i in number_tests) {
var test = number_tests[i];
test.node.style.marginLeft = "50px";
}
from_none_test.style.textIndent = "100px";
from_none_test.style.display = "";
to_none_test.style.textIndent = "100px";
to_none_test.style.display = "none";
always_none_test.style.textIndent = "100px";
// flush style changes
x = getComputedStyle(div, "").color;
gStartTime2 = Date.now(); // set after all transitions have started
gCurrentTime = gStartTime2;
/**
* Assert that a transition whose timing function yields the bezier
* |func|, running from |start_time| to |end_time| (both in seconds
* relative to when the transitions were started) should have produced
* computed value |cval| given that the transition was from
* |start_value| to |end_value| (both numbers in CSS pixels).
*/
function check_transition_value(func, start_time, end_time,
start_value, end_value, cval, desc,
xfail)
{
function value_at(elapsed, error_portion) {
var time_portion = (elapsed - start_time) / (end_time - start_time);
if (time_portion < 0)
time_portion = 0;
else if (time_portion > 1)
time_portion = 1;
var value_portion = func(time_portion) + error_portion;
if (value_portion < 0)
value_portion = 0;
else if (value_portion > 1)
value_portion = 1;
return (1 - value_portion) * start_value + value_portion * end_value;
}
var time_range; // in seconds
var uns_range; // |range| before being sorted (so errors give it
// in the original order
if (gCurrentTime == gStartTime2) {
// No timers involved
time_range = [0, 0];
if (start_time < 0) {
uns_range = [ value_at(0, -0.01), value_at(0, 0.01) ];
} else {
var val = value_at(0, 0);
uns_range = [val, val];
}
} else {
// seconds
// FIXME: Why do we need so much tolerance at the low end of the
// range (primarily for Mac)?
time_range = [ (gCurrentTime - gStartTime2 - 40) / 1000,
(Date.now() - gStartTime1 + 20) / 1000 ];
uns_range = [ value_at(time_range[0], -0.01),
value_at(time_range[1], 0.01) ];
}
var range = uns_range.concat(). /* concat to clone array */
sort(function compareNumbers(a,b) { return a - b; });
var actual = px_to_num(cval);
var fn = xfail ? todo : ok;
fn(range[0] <= actual && actual <= range[1],
desc + ": computed value " + cval + " should be between " +
uns_range[0].toFixed(6) + "px and " + uns_range[1].toFixed(6) +
"px at time between " + time_range[0] + "s and " + time_range[1] + "s.");
}
function check_tf_test()
{
for (var test in tftests) {
var p = tftests[test][0];
var tf = tftests[test][1];
check_transition_value(timingFunctions[tf], 0, 8, 0, 100,
getComputedStyle(p, "").textIndent,
"timing function test for timing function " + tf);
}
check_interrupt_tests();
}
check_tf_test();
add_future_call(2, check_tf_test);
add_future_call(4, check_tf_test);
add_future_call(6, check_tf_test);
add_future_call(8, check_tf_test);
function check_interrupt_tests()
{
for (var test in interrupt_tests) {
var p = interrupt_tests[test][0];
var itime = interrupt_tests[test][1];
check_transition_value(timingFunctions["cubic-bezier(0, 1, 1, 0)"],
0, 8, 0, 100,
getComputedStyle(p, "").textIndent,
"interrupt test for time " + itime + "s");
}
}
// check_interrupt_tests is called from check_tf_test and from
// where we reset the interrupts
function check_delay_test(time)
{
var tf = timingFunctions["ease-out"];
for (var d in delay_tests) {
var p = delay_tests[d];
check_transition_value(tf, Number(d), Number(d) + 4, 0, 100,
getComputedStyle(p, "").marginLeft,
"delay test for delay " + d + "s");
}
}
check_delay_test(0);
for (var i = 1; i <= 8; ++i) {
add_future_call(i, check_delay_test);
}
function reset_reset_test(time)
{
reset_test.style.marginLeft = "0px";
}
function check_reset_test(time)
{
is(getComputedStyle(reset_test, "").marginLeft, "0px",
"reset test value at time " + time + "s.");
}
setTimeout(reset_reset_test, 1000); // must always run, even if late
check_reset_test(0);
for (var i = 1; i <= 8; ++i) {
(function(j) {
add_future_call(j, function() { check_reset_test(j); });
})(i);
}
check_descendant_tests();
add_future_call(2, check_descendant_tests);
add_future_call(6, check_descendant_tests);
function check_descendant_tests() {
// text-indent: transition from 50px to 150px
// letter-spacing: transition from 10px to 5px
var values = {};
values["text-indent"] = [ 50, 150 ];
values["letter-spacing"] = [ 10, 5 ];
var tf = timingFunctions["ease"];
for (var i in descendant_tests) {
var test = descendant_tests[i];
/* ti=text-indent, ls=letter-spacing */
var child_ti_duration = 0;
var child_ls_duration = 0;
var child_ti_delay = 0;
var child_ls_delay = 0;
if (test.parent_transition != "") {
var props = test.parent_transition.split(" ");
var duration = parseInt(props[0]);
var delay = (props.length > 2) ? parseInt(props[2]) : 0;
var property = props[1];
if (property == "text-indent") {
child_ti_duration = duration;
child_ti_delay = delay;
} else if (property == "letter-spacing") {
child_ls_duration = duration;
child_ls_delay = delay;
} else {
ok(false, "fix this test (unexpected transition-property " +
property + " on parent)");
}
}
if (test.child_transition != "") {
var props = test.child_transition.split(" ");
var duration = parseInt(props[0]);
var delay = (props.length > 2) ? parseInt(props[2]) : 0;
var property = props[1];
if (property != "text-indent" && property != "letter-spacing" &&
property != "all") {
ok(false, "fix this test (unexpected transition-property " +
property + " on child)");
}
if (property != "letter-spacing" && child_ti_duration == 0) {
child_ti_duration = duration;
child_ti_delay = delay;
}
if (property != "text-indent" && child_ls_duration == 0) {
child_ls_duration = duration;
child_ls_delay = delay;
}
}
var time_portions = {
"text-indent":
{ duration: child_ti_duration, delay: child_ti_delay },
"letter-spacing":
{ duration: child_ls_duration, delay: child_ls_delay },
};
for (var prop in {"text-indent": true, "letter-spacing": true}) {
var time_portion = time_portions[prop];
if (time_portion.duration == 0) {
time_portion.duration = 0.01;
time_portion.delay = -1;
}
check_transition_value(tf, time_portion.delay,
time_portion.delay + time_portion.duration,
values[prop][0], values[prop][1],
test.childCS.getPropertyValue(prop),
"descendant test, property " + prop);
}
}
}
function check_number_tests()
{
var tf = timingFunctions["ease"];
for (var d in number_tests) {
var test = number_tests[d];
var p = test.node;
check_transition_value(tf, 0, 8, 100, 50,
getComputedStyle(p, "").marginLeft,
"number of transitions test for style " +
test.style);
}
}
check_number_tests(0);
add_future_call(2, check_number_tests);
add_future_call(4, check_number_tests);
add_future_call(6, check_number_tests);
add_future_call(8, check_number_tests);
function check_display_tests(time)
{
var tf = timingFunctions["ease-in-out"];
for (var i in display_tests) {
var p = display_tests[i];
check_transition_value(tf, 0, 8, 0, 100,
getComputedStyle(p, "").textIndent,
"display test for test with " +
p.childNodes[0].data,
// TODO: Making transitions work on 'display:none' elements is
// still not implemented.
(time != 8));
}
}
check_display_tests(0);
add_future_call(2, function() { check_display_tests(2); });
add_future_call(4, function() { check_display_tests(4); });
add_future_call(6, function() { check_display_tests(6); });
add_future_call(8, function() { check_display_tests(8); });
</script>
</pre>
</body>
</html>

View File

@ -211,6 +211,18 @@ public:
NS_ASSERTION(!aOther.IsNull(), "Cannot compute with aOther null value");
return mValue > aOther.mValue;
}
PRBool operator==(const TimeStamp& aOther) const {
// Maybe it's ok to check == with null timestamps?
NS_ASSERTION(!IsNull(), "Cannot compute with a null value");
NS_ASSERTION(!aOther.IsNull(), "Cannot compute with aOther null value");
return mValue == aOther.mValue;
}
PRBool operator!=(const TimeStamp& aOther) const {
// Maybe it's ok to check != with null timestamps?
NS_ASSERTION(!IsNull(), "Cannot compute with a null value");
NS_ASSERTION(!aOther.IsNull(), "Cannot compute with aOther null value");
return mValue != aOther.mValue;
}
// Comparing TimeStamps for equality should be discouraged. Adding
// two TimeStamps, or scaling TimeStamps, is nonsense and must never