--- a/devtools/client/animationinspector/animation-inspector.xhtml
+++ b/devtools/client/animationinspector/animation-inspector.xhtml
@@ -2,16 +2,17 @@
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
<link rel="stylesheet" href="chrome://devtools/skin/animationinspector.css" type="text/css"/>
+ <link rel="stylesheet" href="resource://devtools/client/shared/components/splitter/split-box.css"/>
<script type="application/javascript" src="chrome://devtools/content/shared/theme-switching.js"/>
</head>
<body class="theme-sidebar devtools-monospace" role="application" empty="true">
<div id="global-toolbar" class="theme-toolbar">
<span id="all-animations-label" class="label"></span>
<button id="toggle-all" class="devtools-button pause-button"></button>
</div>
<div id="timeline-toolbar" class="theme-toolbar">
@@ -21,12 +22,22 @@
<span id="timeline-current-time" class="label"></span>
</div>
<div id="players"></div>
<div id="error-message">
<p id="error-type"></p>
<p id="error-hint"></p>
<button id="element-picker" data-standalone="true" class="devtools-button"></button>
</div>
+ <script type="text/javascript">
+ /* eslint-disable */
+ var isInChrome = window.location.href.includes("chrome:");
+ if (isInChrome) {
+ var exports = {};
+ var Cu = Components.utils;
+ var { require } = Cu.import("resource://devtools/shared/Loader.jsm", {});
+ var { BrowserLoader } = Cu.import("resource://devtools/client/shared/browser-loader.js", {});
+ }
+ </script>
<script type="application/javascript" src="animation-controller.js"></script>
<script type="application/javascript" src="animation-panel.js"></script>
</body>
</html>
--- a/devtools/client/animationinspector/components/animation-details.js
+++ b/devtools/client/animationinspector/components/animation-details.js
@@ -200,22 +200,22 @@ AnimationDetails.prototype = {
// Relay the event up, it's needed in parents too.
this.emit(e, args);
},
renderAnimatedPropertiesHeader: function () {
// Add animated property header.
const headerEl = createNode({
parent: this.containerEl,
- attributes: { "class": "animated-properties-header property" }
+ attributes: { "class": "animated-properties-header" }
});
// Add progress tick container.
const progressTickContainerEl = createNode({
- parent: headerEl,
+ parent: this.containerEl,
attributes: { "class": "progress-tick-container track-container" }
});
// Add label container.
const headerLabelContainerEl = createNode({
parent: headerEl,
attributes: { "class": "track-container" }
});
--- a/devtools/client/animationinspector/components/animation-time-block.js
+++ b/devtools/client/animationinspector/components/animation-time-block.js
@@ -2,17 +2,18 @@
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
const EventEmitter = require("devtools/shared/event-emitter");
-const {createNode, TimeScale} = require("devtools/client/animationinspector/utils");
+const {createNode, TimeScale, getFormattedAnimationTitle} =
+ require("devtools/client/animationinspector/utils");
const { LocalizationHelper } = require("devtools/shared/l10n");
const L10N =
new LocalizationHelper("devtools/client/locales/animationinspector.properties");
// In the createPathSegments function, an animation duration is divided by
// DURATION_RESOLUTION in order to draw the way the animation progresses.
// But depending on the timing-function, we may be not able to make the graph
@@ -350,40 +351,16 @@ AnimationTimeBlock.prototype = {
},
get win() {
return this.containerEl.ownerDocument.defaultView;
}
};
/**
- * Get a formatted title for this animation. This will be either:
- * "some-name", "some-name : CSS Transition", "some-name : CSS Animation",
- * "some-name : Script Animation", or "Script Animation", depending
- * if the server provides the type, what type it is and if the animation
- * has a name
- * @param {AnimationPlayerFront} animation
- */
-function getFormattedAnimationTitle({state}) {
- // Older servers don't send a type, and only know about
- // CSSAnimations and CSSTransitions, so it's safe to use
- // just the name.
- if (!state.type) {
- return state.name;
- }
-
- // Script-generated animations may not have a name.
- if (state.type === "scriptanimation" && !state.name) {
- return L10N.getStr("timeline.scriptanimation.unnamedLabel");
- }
-
- return L10N.getFormatStr(`timeline.${state.type}.nameLabel`, state.name);
-}
-
-/**
* Render delay section.
* @param {Element} parentEl - Parent element of this appended path element.
* @param {Object} state - State of animation.
* @param {Object} segmentHelper - The object returned by getSegmentHelper.
*/
function renderDelay(parentEl, state, segmentHelper) {
const startSegment = segmentHelper.getSegment(0);
const endSegment = { x: state.delay, y: startSegment.y };
--- a/devtools/client/animationinspector/components/animation-timeline.js
+++ b/devtools/client/animationinspector/components/animation-timeline.js
@@ -5,22 +5,27 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
const EventEmitter = require("devtools/shared/event-emitter");
const {
createNode,
findOptimalTimeInterval,
+ getFormattedAnimationTitle,
TimeScale
} = require("devtools/client/animationinspector/utils");
const {AnimationDetails} = require("devtools/client/animationinspector/components/animation-details");
const {AnimationTargetNode} = require("devtools/client/animationinspector/components/animation-target-node");
const {AnimationTimeBlock} = require("devtools/client/animationinspector/components/animation-time-block");
+const { LocalizationHelper } = require("devtools/shared/l10n");
+const L10N =
+ new LocalizationHelper("devtools/client/locales/animationinspector.properties");
+
// The minimum spacing between 2 time graduation headers in the timeline (px).
const TIME_GRADUATION_MIN_SPACING = 40;
// When the container window is resized, the timeline background gets refreshed,
// but only after a timer, and the timer is reset if the window is continuously
// resized.
const TIMELINE_BACKGROUND_RESIZE_DEBOUNCE_TIMER = 50;
/**
@@ -37,17 +42,16 @@ const TIMELINE_BACKGROUND_RESIZE_DEBOUNC
*
* @param {InspectorPanel} inspector.
* @param {Object} serverTraits The list of server-side capabilities.
*/
function AnimationsTimeline(inspector, serverTraits) {
this.animations = [];
this.targetNodes = [];
this.timeBlocks = [];
- this.details = [];
this.inspector = inspector;
this.serverTraits = serverTraits;
this.onAnimationStateChanged = this.onAnimationStateChanged.bind(this);
this.onScrubberMouseDown = this.onScrubberMouseDown.bind(this);
this.onScrubberMouseUp = this.onScrubberMouseUp.bind(this);
this.onScrubberMouseOut = this.onScrubberMouseOut.bind(this);
this.onScrubberMouseMove = this.onScrubberMouseMove.bind(this);
@@ -58,103 +62,193 @@ function AnimationsTimeline(inspector, s
EventEmitter.decorate(this);
}
exports.AnimationsTimeline = AnimationsTimeline;
AnimationsTimeline.prototype = {
init: function (containerEl) {
this.win = containerEl.ownerDocument.defaultView;
+ this.rootWrapperEl = containerEl;
- this.rootWrapperEl = createNode({
- parent: containerEl,
- attributes: {
- "class": "animation-timeline"
- }
+ this.setupSplitBox();
+ this.setupAnimationTimeline();
+ this.setupAnimationDetail();
+
+ this.win.addEventListener("resize",
+ this.onWindowResize);
+ },
+
+ setupSplitBox: function () {
+ const browserRequire = this.win.BrowserLoader({
+ window: this.win,
+ useOnlyShared: true
+ }).require;
+
+ const React = browserRequire("devtools/client/shared/vendor/react");
+ const ReactDOM = browserRequire("devtools/client/shared/vendor/react-dom");
+
+ const SplitBox = React.createFactory(
+ browserRequire("devtools/client/shared/components/splitter/split-box"));
+
+ const splitter = SplitBox({
+ className: "animation-root",
+ initialSize: "0 0",
+ maxSize: "calc(100% - (var(--timeline-animation-height) * 2))",
+ splitterSize: 1,
+ endPanelControl: true,
+ startPanel: React.DOM.div({
+ className: "animation-timeline"
+ }),
+ endPanel: React.DOM.div({
+ className: "animation-detail"
+ }),
+ vert: false
});
+ ReactDOM.render(splitter, this.rootWrapperEl);
+ },
+
+ setupAnimationTimeline: function () {
+ const animationTimelineEl = this.rootWrapperEl.querySelector(".animation-timeline");
+
let scrubberContainer = createNode({
- parent: this.rootWrapperEl,
+ parent: animationTimelineEl,
attributes: {"class": "scrubber-wrapper"}
});
this.scrubberEl = createNode({
parent: scrubberContainer,
attributes: {
"class": "scrubber"
}
});
this.scrubberHandleEl = createNode({
parent: this.scrubberEl,
attributes: {
"class": "scrubber-handle"
}
});
+ createNode({
+ parent: this.scrubberHandleEl,
+ attributes: {
+ "class": "scrubber-line"
+ }
+ });
this.scrubberHandleEl.addEventListener("mousedown",
- this.onScrubberMouseDown);
+ this.onScrubberMouseDown);
this.headerWrapper = createNode({
- parent: this.rootWrapperEl,
+ parent: animationTimelineEl,
attributes: {
"class": "header-wrapper"
}
});
this.timeHeaderEl = createNode({
parent: this.headerWrapper,
attributes: {
"class": "time-header track-container"
}
});
this.timeHeaderEl.addEventListener("mousedown",
- this.onScrubberMouseDown);
+ this.onScrubberMouseDown);
this.timeTickEl = createNode({
- parent: this.rootWrapperEl,
+ parent: animationTimelineEl,
attributes: {
"class": "time-body track-container"
}
});
this.animationsEl = createNode({
- parent: this.rootWrapperEl,
+ parent: animationTimelineEl,
nodeType: "ul",
attributes: {
"class": "animations"
}
});
+ },
- this.win.addEventListener("resize",
- this.onWindowResize);
+ setupAnimationDetail: function () {
+ this.animationDetailEl = this.rootWrapperEl.querySelector(".animation-detail");
+
+ this.animationDetailEl.dataset.defaultDisplayStyle =
+ this.win.getComputedStyle(this.animationDetailEl).display;
+ this.animationDetailEl.style.display = "none";
+
+ const animationDetailHeaderEl = createNode({
+ parent: this.animationDetailEl,
+ attributes: {
+ "class": "animation-detail-header"
+ }
+ });
+
+ const headerTitleEl = createNode({
+ parent: animationDetailHeaderEl,
+ attributes: {
+ "class": "devtools-toolbar"
+ }
+ });
+
+ createNode({
+ parent: headerTitleEl,
+ textContent: L10N.getStr("detail.headerTitle")
+ });
+
+ this.animationAnimationNameEl = createNode({
+ parent: headerTitleEl
+ });
+
+ const animationDetailBodyEl = createNode({
+ parent: this.animationDetailEl,
+ attributes: {
+ "class": "animation-detail-body"
+ }
+ });
+
+ this.animatedPropertiesEl = createNode({
+ parent: animationDetailBodyEl,
+ attributes: {
+ "class": "animated-properties"
+ }
+ });
+
+ this.details = new AnimationDetails(this.serverTraits);
+ this.details.init(this.animatedPropertiesEl);
},
destroy: function () {
this.stopAnimatingScrubber();
this.unrender();
+ this.details.destroy();
this.win.removeEventListener("resize",
this.onWindowResize);
this.timeHeaderEl.removeEventListener("mousedown",
this.onScrubberMouseDown);
this.scrubberHandleEl.removeEventListener("mousedown",
this.onScrubberMouseDown);
this.rootWrapperEl.remove();
this.animations = [];
-
this.rootWrapperEl = null;
this.timeHeaderEl = null;
this.animationsEl = null;
+ this.animatedPropertiesEl = null;
this.scrubberEl = null;
this.scrubberHandleEl = null;
this.win = null;
this.inspector = null;
this.serverTraits = null;
+ this.animationDetailEl = null;
+ this.animationAnimationNameEl = null;
+ this.animatedPropertiesEl = null;
},
/**
* Destroy sub-components that have been created and stored on this instance.
* @param {String} name An array of components will be expected in this[name]
* @param {Array} handlers An option list of event handlers information that
* should be used to remove these handlers.
*/
@@ -171,20 +265,18 @@ AnimationsTimeline.prototype = {
unrender: function () {
for (let animation of this.animations) {
animation.off("changed", this.onAnimationStateChanged);
}
this.stopAnimatingScrubber();
TimeScale.reset();
this.destroySubComponents("targetNodes");
this.destroySubComponents("timeBlocks");
- this.destroySubComponents("details", [{
- event: "frame-selected",
- fn: this.onFrameSelected
- }]);
+ this.details.off("frame-selected", this.onFrameSelected);
+ this.details.unrender();
this.animationsEl.innerHTML = "";
},
onWindowResize: function () {
// Don't do anything if the root element has a width of 0
if (this.rootWrapperEl.offsetWidth === 0) {
return;
}
@@ -201,28 +293,37 @@ AnimationsTimeline.prototype = {
onAnimationSelected: function (e, animation) {
let index = this.animations.indexOf(animation);
if (index === -1) {
return;
}
let el = this.rootWrapperEl;
let animationEl = el.querySelectorAll(".animation")[index];
- let propsEl = el.querySelectorAll(".animated-properties")[index];
// Toggle the selected state on this animation.
animationEl.classList.toggle("selected");
- propsEl.classList.toggle("selected");
// Render the details component for this animation if it was shown.
if (animationEl.classList.contains("selected")) {
- this.details[index].render(animation);
+ // Add class of animation type.
+ if (!this.animatedPropertiesEl.classList.contains(animation.state.type)) {
+ this.animatedPropertiesEl.className =
+ `animated-properties ${ animation.state.type }`;
+ }
+ this.animationDetailEl.style.display =
+ this.animationDetailEl.dataset.defaultDisplayStyle;
+ this.details.render(animation);
this.emit("animation-selected", animation);
+
+ this.animationAnimationNameEl.textContent =
+ getFormattedAnimationTitle(animation);
} else {
this.emit("animation-unselected", animation);
+ this.animationDetailEl.style.display = "none";
}
},
/**
* When a frame gets selected, move the scrubber to the corresponding position
*/
onFrameSelected: function (e, {x}) {
this.moveScrubberTo(x, true);
@@ -326,31 +427,16 @@ AnimationsTimeline.prototype = {
nodeType: "li",
attributes: {
"class": "animation " +
animation.state.type +
this.getCompositorStatusClassName(animation.state)
}
});
- // Right below the line is a hidden-by-default line for displaying the
- // inline keyframes.
- let detailsEl = createNode({
- parent: this.animationsEl,
- nodeType: "li",
- attributes: {
- "class": "animated-properties " + animation.state.type
- }
- });
-
- let details = new AnimationDetails(this.serverTraits);
- details.init(detailsEl);
- details.on("frame-selected", this.onFrameSelected);
- this.details.push(details);
-
// Left sidebar for the animated node.
let animatedNodeEl = createNode({
parent: animationEl,
attributes: {
"class": "target"
}
});
@@ -371,16 +457,17 @@ AnimationsTimeline.prototype = {
// Draw the animation time block.
let timeBlock = new AnimationTimeBlock();
timeBlock.init(timeBlockEl);
timeBlock.render(animation);
this.timeBlocks.push(timeBlock);
timeBlock.on("selected", this.onAnimationSelected);
}
+ this.details.on("frame-selected", this.onFrameSelected);
// Use the document's current time to position the scrubber (if the server
// doesn't provide it, hide the scrubber entirely).
// Note that because the currentTime was sent via the protocol, some time
// may have gone by since then, and so the scrubber might be a bit late.
if (!documentCurrentTime) {
this.scrubberEl.style.display = "none";
} else {
--- a/devtools/client/animationinspector/test/browser_animation_animated_properties_displayed.js
+++ b/devtools/client/animationinspector/test/browser_animation_animated_properties_displayed.js
@@ -47,22 +47,16 @@ add_task(function* () {
"The list of properties panel is shown");
ok(propertiesList.querySelectorAll(".property").length,
"The list of properties panel actually contains properties");
ok(hasExpectedProperties(propertiesList),
"The list of properties panel contains the right properties");
ok(hasExpectedWarnings(propertiesList),
"The list of properties panel contains the right warnings");
-
- info("Click to unselect the animation");
- yield clickOnAnimation(panel, 0, true);
-
- ok(!isNodeVisible(propertiesList),
- "The list of properties panel is hidden again");
});
function hasExpectedProperties(containerEl) {
let names = [...containerEl.querySelectorAll(".property .name")]
.map(n => n.textContent)
.sort();
if (names.length !== EXPECTED_PROPERTIES.length) {
--- a/devtools/client/animationinspector/test/browser_animation_click_selects_animation.js
+++ b/devtools/client/animationinspector/test/browser_animation_click_selects_animation.js
@@ -32,13 +32,10 @@ add_task(function* () {
info("Click again on the first animation and check if it unselects");
yield clickOnAnimation(panel, 0, true);
ok(!isTimeBlockSelected(timeline, 0),
"The first time block has been unselected");
});
function isTimeBlockSelected(timeline, index) {
let animation = timeline.rootWrapperEl.querySelectorAll(".animation")[index];
- let animatedProperties = timeline.rootWrapperEl.querySelectorAll(
- ".animated-properties")[index];
- return animation.classList.contains("selected") &&
- animatedProperties.classList.contains("selected");
+ return animation.classList.contains("selected");
}
--- a/devtools/client/animationinspector/test/browser_animation_keyframe_click_to_set_time.js
+++ b/devtools/client/animationinspector/test/browser_animation_keyframe_click_to_set_time.js
@@ -17,32 +17,32 @@ add_task(function* () {
// the animations to be slightly offset with the header when it appears.
// So for now, let's hide the scrollbar. Bug 1229340 should fix this.
timeline.animationsEl.style.overflow = "hidden";
info("Expand the animation");
yield clickOnAnimation(panel, 0);
info("Click on the first keyframe of the first animated property");
- yield clickKeyframe(panel, 0, "background-color", 0);
+ yield clickKeyframe(panel, "background-color", 0);
info("Make sure the scrubber stopped moving and is at the right position");
yield assertScrubberMoving(panel, false);
checkScrubberPos(scrubberEl, 0);
info("Click on a keyframe in the middle");
- yield clickKeyframe(panel, 0, "transform", 2);
+ yield clickKeyframe(panel, "transform", 2);
info("Make sure the scrubber is at the right position");
checkScrubberPos(scrubberEl, 50);
});
-function* clickKeyframe(panel, animIndex, property, index) {
- let keyframeComponent = getKeyframeComponent(panel, animIndex, property);
- let keyframeEl = getKeyframeEl(panel, animIndex, property, index);
+function* clickKeyframe(panel, property, index) {
+ let keyframeComponent = getKeyframeComponent(panel, property);
+ let keyframeEl = getKeyframeEl(panel, property, index);
let onSelect = keyframeComponent.once("frame-selected");
EventUtils.sendMouseEvent({type: "click"}, keyframeEl,
keyframeEl.ownerDocument.defaultView);
yield onSelect;
}
function checkScrubberPos(scrubberEl, pos) {
--- a/devtools/client/animationinspector/test/browser_animation_timeline_iterationStart.js
+++ b/devtools/client/animationinspector/test/browser_animation_timeline_iterationStart.js
@@ -6,32 +6,32 @@
// Check that the iteration start is displayed correctly in time blocks.
add_task(function* () {
yield addTab(URL_ROOT + "doc_script_animation.html");
let {panel} = yield openAnimationInspector();
let timelineComponent = panel.animationsTimelineComponent;
let timeBlockComponents = timelineComponent.timeBlocks;
- let detailsComponents = timelineComponent.details;
+ let detailsComponent = timelineComponent.details;
for (let i = 0; i < timeBlockComponents.length; i++) {
info(`Expand time block ${i} so its keyframes are visible`);
yield clickOnAnimation(panel, i);
info(`Check the state of time block ${i}`);
let {containerEl, animation: {state}} = timeBlockComponents[i];
checkAnimationTooltip(containerEl, state);
checkProgressAtStartingTime(containerEl, state);
// Get the first set of keyframes (there's only one animated property
// anyway), and the first frame element from there, we're only interested in
// its offset.
- let keyframeComponent = detailsComponents[i].keyframeComponents[0];
+ let keyframeComponent = detailsComponent.keyframeComponents[0];
let frameEl = keyframeComponent.keyframesEl.querySelector(".frame");
checkKeyframeOffset(containerEl, frameEl, state);
}
});
function checkAnimationTooltip(el, {iterationStart, duration}) {
info("Check an animation's iterationStart data in its tooltip");
let title = el.querySelector(".name").getAttribute("title");
--- a/devtools/client/animationinspector/test/head.js
+++ b/devtools/client/animationinspector/test/head.js
@@ -380,47 +380,44 @@ function* clickOnAnimation(panel, index,
let onSelectionChanged = timeline.once(shouldClose
? "animation-unselected"
: "animation-selected");
// If we're opening the animation, also wait for
// the animation-detail-rendering-completed event.
let onReady = shouldClose
? Promise.resolve()
- : timeline.details[index].once("animation-detail-rendering-completed");
+ : timeline.details.once("animation-detail-rendering-completed");
info("Click on animation " + index + " in the timeline");
let timeBlock = timeline.rootWrapperEl.querySelectorAll(".time-block")[index];
EventUtils.sendMouseEvent({type: "click"}, timeBlock,
timeBlock.ownerDocument.defaultView);
yield onReady;
return yield onSelectionChanged;
}
/**
* Get an instance of the Keyframes component from the timeline.
* @param {AnimationsPanel} panel The panel instance.
- * @param {Number} animationIndex The index of the animation in the timeline.
* @param {String} propertyName The name of the animated property.
* @return {Keyframes} The Keyframes component instance.
*/
-function getKeyframeComponent(panel, animationIndex, propertyName) {
+function getKeyframeComponent(panel, propertyName) {
let timeline = panel.animationsTimelineComponent;
- let detailsComponent = timeline.details[animationIndex];
+ let detailsComponent = timeline.details;
return detailsComponent.keyframeComponents
.find(c => c.propertyName === propertyName);
}
/**
* Get a keyframe element from the timeline.
* @param {AnimationsPanel} panel The panel instance.
- * @param {Number} animationIndex The index of the animation in the timeline.
* @param {String} propertyName The name of the animated property.
* @param {Index} keyframeIndex The index of the keyframe.
* @return {DOMNode} The keyframe element.
*/
-function getKeyframeEl(panel, animationIndex, propertyName, keyframeIndex) {
- let keyframeComponent = getKeyframeComponent(panel, animationIndex,
- propertyName);
+function getKeyframeEl(panel, propertyName, keyframeIndex) {
+ let keyframeComponent = getKeyframeComponent(panel, propertyName);
return keyframeComponent.keyframesEl
.querySelectorAll(".frame")[keyframeIndex];
}
--- a/devtools/client/animationinspector/utils.js
+++ b/devtools/client/animationinspector/utils.js
@@ -303,8 +303,33 @@ function getJsPropertyName(cssPropertyNa
return "cssFloat";
}
// https://drafts.csswg.org/cssom/#css-property-to-idl-attribute
return cssPropertyName.replace(/-([a-z])/gi, (str, group) => {
return group.toUpperCase();
});
}
exports.getJsPropertyName = getJsPropertyName;
+
+/**
+ * Get a formatted title for this animation. This will be either:
+ * "some-name", "some-name : CSS Transition", "some-name : CSS Animation",
+ * "some-name : Script Animation", or "Script Animation", depending
+ * if the server provides the type, what type it is and if the animation
+ * has a name
+ * @param {AnimationPlayerFront} animation
+ */
+function getFormattedAnimationTitle({state}) {
+ // Older servers don't send a type, and only know about
+ // CSSAnimations and CSSTransitions, so it's safe to use
+ // just the name.
+ if (!state.type) {
+ return state.name;
+ }
+
+ // Script-generated animations may not have a name.
+ if (state.type === "scriptanimation" && !state.name) {
+ return L10N.getStr("timeline.scriptanimation.unnamedLabel");
+ }
+
+ return L10N.getFormatStr(`timeline.${state.type}.nameLabel`, state.name);
+}
+exports.getFormattedAnimationTitle = getFormattedAnimationTitle;
--- a/devtools/client/locales/en-US/animationinspector.properties
+++ b/devtools/client/locales/en-US/animationinspector.properties
@@ -173,8 +173,12 @@ timeline.scriptanimation.unnamedLabel=Sc
# %S will be replaced by the name of the transition at run-time.
timeline.unknown.nameLabel=%S
# LOCALIZATION NOTE (detail.propertiesHeader.percentage):
# This string is displayed on header label in .animated-properties-header.
# %S represents the value in percentage with two decimal points, localized.
# there are two "%" after %S to escape and display "%"
detail.propertiesHeader.percentage=%S%%
+
+# LOCALIZATION NOTE (detail.headerTitle):
+# This string is displayed on header label in .animation-detail-header.
+detail.headerTitle=Animated properties for
--- a/devtools/client/themes/animationinspector.css
+++ b/devtools/client/themes/animationinspector.css
@@ -134,18 +134,16 @@ body {
#sidebar-panel-animationinspector {
height: 100%;
width: 100%;
}
#players {
height: calc(100% - var(--toolbar-height));
- overflow-x: hidden;
- overflow-y: auto;
}
[empty] #players {
display: none;
}
/* The error message, shown when an invalid/unanimated element is selected */
@@ -225,79 +223,86 @@ body {
padding-right: 1em;
}
#timeline-rate {
position: relative;
width: 4.5em;
}
+.animation-root > .uncontrolled {
+ overflow: hidden;
+}
+
/* Animation timeline component */
.animation-timeline {
position: relative;
- display: flex;
- flex-direction: column;
+ width: 100%;
+ overflow: auto;
}
/* Useful for positioning animations or keyframes in the timeline */
+.animation-detail .track-container,
.animation-timeline .track-container {
position: absolute;
top: 0;
left: var(--timeline-sidebar-width);
/* Leave the width of a marker right of a track so the 100% markers can be
selected easily */
right: var(--keyframes-marker-size);
height: var(--timeline-animation-height);
}
.animation-timeline .scrubber-wrapper {
position: absolute;
+ z-index: 5;
left: var(--timeline-sidebar-width);
/* Leave the width of a marker right of a track so the 100% markers can be
selected easily */
right: var(--keyframes-marker-size);
- height: 100%;
+ pointer-events: none;
}
.animation-timeline .scrubber {
- z-index: 5;
pointer-events: none;
position: absolute;
- /* Make the scrubber as tall as the viewport minus the toolbar height and the
- header-wrapper's borders */
- height: calc(100vh - var(--toolbar-height) - 1px);
- min-height: 100%;
width: 0;
- border-right: 1px solid red;
- box-sizing: border-box;
+ margin-left: -6px;
}
/* The scrubber handle is a transparent element displayed on top of the scrubber
line that allows users to drag it */
.animation-timeline .scrubber .scrubber-handle {
- position: absolute;
+ position: fixed;
height: 100%;
/* Make it thick enough for easy dragging */
- width: 6px;
- right: -1.5px;
+ width: 12px;
cursor: col-resize;
pointer-events: all;
}
.animation-timeline .scrubber .scrubber-handle::before {
content: "";
- position: sticky;
+ position: absolute;
top: 0;
width: 1px;
border-top: 5px solid red;
border-left: 5px solid transparent;
border-right: 5px solid transparent;
}
+.animation-timeline .scrubber .scrubber-handle .scrubber-line {
+ position: relative;
+ height: 100%;
+ left: 5px;
+ width: 0;
+ border-right: 1px solid red;
+}
+
.animation-timeline .time-header {
min-height: var(--timeline-animation-height);
cursor: col-resize;
-moz-user-select: none;
}
.animated-properties-header .header-item,
.animation-timeline .time-header .header-item {
@@ -309,59 +314,65 @@ body {
.animation-timeline .header-wrapper {
position: sticky;
top: 0;
background-color: var(--theme-body-background);
border-bottom: 1px solid var(--time-graduation-border-color);
z-index: 3;
height: var(--timeline-animation-height);
+ width: 100%;
overflow: hidden;
}
.animation-timeline .time-body {
- height: 100%;
+ top: var(--timeline-animation-height);
}
.progress-tick-container .progress-tick,
.animation-timeline .time-body .time-tick {
-moz-user-select: none;
position: absolute;
+ height: 100%;
+}
+
+.progress-tick-container .progress-tick::before,
+.animation-timeline .time-body .time-tick::before {
+ content: "";
+ position: fixed;
+ height: 100vh;
width: 0;
- /* When scroll bar is shown, make it covers entire time-body */
- height: 100%;
- /* When scroll bar is hidden, make it as tall as the viewport minus the
- timeline animation height and the header-wrapper's borders */
- min-height: calc(100vh - var(--timeline-animation-height) - 1px);
border-left: 0.5px solid var(--time-graduation-border-color);
}
.animation-timeline .animations {
+ position: relative;
width: 100%;
- height: 100%;
padding: 0;
list-style-type: none;
margin-top: 0;
}
/* Animation block widgets */
.animation-timeline .animation {
margin: 2px 0;
height: var(--timeline-animation-height);
position: relative;
}
-/* We want animations' background colors to alternate, but each animation has
- a sibling (hidden by default) that contains the animated properties and
- keyframes, so we need to alternate every 4 elements. */
-.animation-timeline .animation:nth-child(4n+1) {
+/* Display animations' background colors to alternate. */
+.animation-timeline .animation:nth-child(2n+1) {
background-color: var(--even-animation-timeline-background-color);
}
+.animation-timeline .animation:last-child {
+ margin-bottom: calc(var(--timeline-animation-height) / 2);
+}
+
.animation-timeline .animation .target {
width: var(--timeline-sidebar-width);
height: 100%;
overflow: hidden;
display: flex;
align-items: center;
}
@@ -482,17 +493,16 @@ body {
.animation-timeline .animation .fill.delay::after,
.animation-timeline .animation .fill.end-delay::after {
border-color: var(--fill-enable-color);
background-color: var(--fill-enable-color);
}
/* Animation target node gutter, contains a preview of the dom node */
-
.animation-target {
background-color: var(--theme-toolbar-background);
padding: 0 4px;
box-sizing: border-box;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
cursor: pointer;
@@ -515,77 +525,69 @@ body {
.animation-target .node-highlighter:active,
.animation-target .node-highlighter.selected {
filter: url(images/filters.svg#checked-icon-state) brightness(0.9);
}
/* Inline keyframes info in the timeline */
-.animation-timeline .animated-properties:not(.selected) {
- display: none;
-}
-
-.animation-timeline .animated-properties {
- background-color: var(--theme-selection-background-semitransparent);
-}
-
-.animation-timeline .animated-properties .property {
+.animation-detail .animated-properties .property {
height: var(--timeline-animation-height);
position: relative;
}
-.animation-timeline .animated-properties .property:nth-child(2n) {
+.animation-detail .animated-properties .property:nth-child(2n) {
background-color: var(--even-animation-timeline-background-color);
}
-.animation-timeline .animated-properties .name {
+.animation-detail .animated-properties .name {
width: var(--timeline-sidebar-width);
padding-right: var(--keyframes-marker-size);
box-sizing: border-box;
height: 100%;
color: var(--theme-body-color-alt);
white-space: nowrap;
display: flex;
justify-content: flex-end;
align-items: center;
}
-.animation-timeline .animated-properties .name div {
+.animation-detail .animated-properties .name div {
overflow: hidden;
text-overflow: ellipsis;
}
-.animated-properties.cssanimation {
+.animation-detail .animated-properties.cssanimation {
--background-color: var(--theme-contrast-background);
}
-.animated-properties.csstransition {
+.animation-detail .animated-properties.csstransition {
--background-color: var(--theme-highlight-blue);
}
-.animated-properties.scriptanimation {
+.animation-detail .animated-properties.scriptanimation {
--background-color: var(--theme-graphs-green);
}
-.animation-timeline .animated-properties .oncompositor::before {
+.animation-detail .animated-properties .oncompositor::before {
content: "";
display: inline-block;
width: 17px;
height: 17px;
background-color: var(--background-color);
clip-path: url(images/animation-fast-track.svg#thunderbolt);
vertical-align: middle;
}
-.animation-timeline .animated-properties .warning {
+.animation-detail .animated-properties .warning {
text-decoration: underline dotted;
}
-.animation-timeline .animated-properties .frames {
+.animation-detail .animated-properties .frames {
/* The frames list is absolutely positioned and the left and width properties
are dynamically set from javascript to match the animation's startTime and
duration */
position: absolute;
top: 0;
left: 0;
height: 100%;
width: 100%;
@@ -601,17 +603,16 @@ body {
/* Actual keyframe markers are positioned absolutely within this container and
their position is relative to its size (we know the offset of each frame
in percentage) */
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
-
}
.keyframes .frame {
position: absolute;
top: 50%;
width: 0;
height: 0;
background-color: inherit;
@@ -665,32 +666,88 @@ body {
.keyframes svg path.transform {
fill: var(--transform-background-color);
stroke: var(--transform-border-color);
}
.keyframes svg path.color {
stroke: none;
+ height: 100%;
+}
+
+.animation-detail {
+ position: relative;
+ width: 100%;
+ background-color: var(--theme-body-background);
+ z-index: 5;
+}
+
+.animation-detail .animation-detail-header {
+ height: var(--toolbar-height);
+ width: 100%;
+}
+
+.animation-detail .animation-detail-header > div {
+ position: fixed;
+ display: flex;
+ flex-wrap: nowrap;
+ width: 100%;
+ height: var(--toolbar-height);
+ line-height: var(--toolbar-height);
+ background-color: var(--theme-body-background);
+ z-index: 5;
+}
+
+.animation-detail .animation-detail-header > div > div {
+ white-space: nowrap;
+}
+
+.animation-detail .animation-detail-header > div > div:first-child {
+ margin-left: 15px;
+}
+
+.animation-detail .animation-detail-header > div > div:nth-child(2) {
+ margin-left: .5em;
+}
+
+.animation-detail .animation-detail-body {
+ position: relative;
+ background-color: var(--theme-body-background);
+}
+
+.animation-detail .animation-detail-body .animated-properties {
+ position: relative;
+ height: 100%;
}
.animated-properties-header {
+ -moz-user-select: none;
+ position: sticky;
+ top: var(--timeline-animation-height);
min-height: var(--timeline-animation-height);
- -moz-user-select: none;
+ padding-top: 2px;
+ z-index: 3;
+ background-color: var(--theme-body-background);
}
.animated-properties-header .header-item:nth-child(2) {
left: 50%;
}
.animated-properties-header .header-item:nth-child(3) {
- right: 0;
+ right: -0.5px;
border-left: none;
border-right: 0.5px solid var(--time-graduation-border-color);
}
.progress-tick-container .progress-tick:nth-child(2) {
left: 50%;
}
.progress-tick-container .progress-tick:nth-child(3) {
left: 100%;
}
+
+.animated-properties-body .property:last-child {
+ /* To display animation progress graph clealy when the scroll is bottom. */
+ padding-bottom: calc(var(--timeline-animation-height) / 2);
+}