Bug 1171863 - Define all positions and sizes in percentage for auto-resize; r=tromey

Instead of having the various positions and sizes of elements of the timeline
defined in pixels, this defines them in % of the total width.
This way the animations, scrubber, etc... adapt as you resize the panel.
The only complex thing here is resizing the header and background. Both of
them are generated via javascript. To do this, they are now positioned and sized
in % too, so they resize when the window is resized, and after a debounced
delay, they get re-generated too.
This commit is contained in:
Patrick Brosset 2015-12-02 13:52:15 +01:00
parent 05371c75fe
commit cc9fdc2169
8 changed files with 124 additions and 98 deletions

View File

@ -36,9 +36,10 @@ const MILLIS_TIME_FORMAT_MAX_DURATION = 4000;
const TIME_GRADUATION_MIN_SPACING = 40;
// List of playback rate presets displayed in the timeline toolbar.
const PLAYBACK_RATES = [.1, .25, .5, 1, 2, 5, 10];
// The size of the fast-track icon (for compositor-running animations), this is
// used to position the icon correctly.
const FAST_TRACK_ICON_SIZE = 20;
// 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;
/**
* UI component responsible for displaying a preview of the target dom node of
@ -477,46 +478,42 @@ var TimeScale = {
},
/**
* Convert a startTime to a distance in pixels, in the current time scale.
* Convert a startTime to a distance in %, in the current time scale.
* @param {Number} time
* @param {Number} containerWidth The width of the container element.
* @return {Number}
*/
startTimeToDistance: function(time, containerWidth) {
startTimeToDistance: function(time) {
time -= this.minStartTime;
return this.durationToDistance(time, containerWidth);
return this.durationToDistance(time);
},
/**
* Convert a duration to a distance in pixels, in the current time scale.
* Convert a duration to a distance in %, in the current time scale.
* @param {Number} time
* @param {Number} containerWidth The width of the container element.
* @return {Number}
*/
durationToDistance: function(duration, containerWidth) {
return containerWidth * duration / (this.maxEndTime - this.minStartTime);
durationToDistance: function(duration) {
return duration * 100 / (this.maxEndTime - this.minStartTime);
},
/**
* Convert a distance in pixels to a time, in the current time scale.
* Convert a distance in % to a time, in the current time scale.
* @param {Number} distance
* @param {Number} containerWidth The width of the container element.
* @return {Number}
*/
distanceToTime: function(distance, containerWidth) {
distanceToTime: function(distance) {
return this.minStartTime +
((this.maxEndTime - this.minStartTime) * distance / containerWidth);
((this.maxEndTime - this.minStartTime) * distance / 100);
},
/**
* Convert a distance in pixels to a time, in the current time scale.
* Convert a distance in % to a time, in the current time scale.
* The time will be relative to the current minimum start time.
* @param {Number} distance
* @param {Number} containerWidth The width of the container element.
* @return {Number}
*/
distanceToRelativeTime: function(distance, containerWidth) {
let time = this.distanceToTime(distance, containerWidth);
distanceToRelativeTime: function(distance) {
let time = this.distanceToTime(distance);
return time - this.minStartTime;
},
@ -560,12 +557,15 @@ function AnimationsTimeline(inspector) {
this.targetNodes = [];
this.timeBlocks = [];
this.inspector = inspector;
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);
this.onAnimationSelected = this.onAnimationSelected.bind(this);
this.onWindowResize = this.onWindowResize.bind(this);
EventEmitter.decorate(this);
}
@ -582,8 +582,13 @@ AnimationsTimeline.prototype = {
}
});
this.scrubberEl = createNode({
let scrubberContainer = createNode({
parent: this.rootWrapperEl,
attributes: {"class": "scrubber-wrapper"}
});
this.scrubberEl = createNode({
parent: scrubberContainer,
attributes: {
"class": "scrubber"
}
@ -612,12 +617,15 @@ AnimationsTimeline.prototype = {
"class": "animations"
}
});
this.win.addEventListener("resize", this.onWindowResize);
},
destroy: function() {
this.stopAnimatingScrubber();
this.unrender();
this.win.removeEventListener("resize", this.onWindowResize);
this.timeHeaderEl.removeEventListener("mousedown",
this.onScrubberMouseDown);
this.scrubberHandleEl.removeEventListener("mousedown",
@ -660,6 +668,16 @@ AnimationsTimeline.prototype = {
this.animationsEl.innerHTML = "";
},
onWindowResize: function() {
if (this.windowResizeTimer) {
this.win.clearTimeout(this.windowResizeTimer);
}
this.windowResizeTimer = this.win.setTimeout(() => {
this.drawHeaderAndBackground();
}, TIMELINE_BACKGROUND_RESIZE_DEBOUNCE_TIMER);
},
onAnimationSelected: function(e, animation) {
// Unselect the previously selected animation if any.
[...this.rootWrapperEl.querySelectorAll(".animation.selected")].forEach(el => {
@ -713,15 +731,18 @@ AnimationsTimeline.prototype = {
moveScrubberTo: function(pageX) {
this.stopAnimatingScrubber();
let offset = pageX - this.scrubberEl.offsetWidth;
// The offset needs to be in % and relative to the timeline's area (so we
// subtract the scrubber's left offset, which is equal to the sidebar's
// width).
let offset = (pageX - this.timeHeaderEl.offsetLeft) * 100 /
this.timeHeaderEl.offsetWidth;
if (offset < 0) {
offset = 0;
}
this.scrubberEl.style.left = offset + "px";
this.scrubberEl.style.left = offset + "%";
let time = TimeScale.distanceToRelativeTime(offset,
this.timeHeaderEl.offsetWidth);
let time = TimeScale.distanceToRelativeTime(offset);
this.emit("timeline-data-changed", {
isPaused: true,
@ -817,8 +838,8 @@ AnimationsTimeline.prototype = {
},
startAnimatingScrubber: function(time) {
let x = TimeScale.startTimeToDistance(time, this.timeHeaderEl.offsetWidth);
this.scrubberEl.style.left = x + "px";
let x = TimeScale.startTimeToDistance(time);
this.scrubberEl.style.left = x + "%";
// Only stop the scrubber if it's out of bounds or all animations have been
// paused, but not if at least an animation is infinite.
@ -833,7 +854,7 @@ AnimationsTimeline.prototype = {
isPaused: !this.isAtLeastOneAnimationPlaying(),
isMoving: false,
isUserDrag: false,
time: TimeScale.distanceToRelativeTime(x, this.timeHeaderEl.offsetWidth)
time: TimeScale.distanceToRelativeTime(x)
});
return;
}
@ -842,7 +863,7 @@ AnimationsTimeline.prototype = {
isPaused: false,
isMoving: true,
isUserDrag: false,
time: TimeScale.distanceToRelativeTime(x, this.timeHeaderEl.offsetWidth)
time: TimeScale.distanceToRelativeTime(x)
});
let now = this.win.performance.now();
@ -878,15 +899,15 @@ AnimationsTimeline.prototype = {
this.timeHeaderEl.innerHTML = "";
let interval = findOptimalTimeInterval(scale, TIME_GRADUATION_MIN_SPACING);
for (let i = 0; i < width; i += interval) {
let pos = 100 * i / width;
createNode({
parent: this.timeHeaderEl,
nodeType: "span",
attributes: {
"class": "time-tick",
"style": `left:${i}px`
"style": `left:${pos}%`
},
textContent: TimeScale.formatTime(
TimeScale.distanceToRelativeTime(i, width))
textContent: TimeScale.formatTime(TimeScale.distanceToRelativeTime(pos))
});
}
}
@ -922,8 +943,6 @@ AnimationTimeBlock.prototype = {
this.animation = animation;
let {state} = this.animation;
let width = this.containerEl.offsetWidth;
// Create a container element to hold the delay and iterations.
// It is positioned according to its delay (divided by the playbackrate),
// and its width is according to its duration (divided by the playbackrate).
@ -933,10 +952,10 @@ AnimationTimeBlock.prototype = {
let count = state.iterationCount;
let delay = state.delay || 0;
let x = TimeScale.startTimeToDistance(start + (delay / rate), width);
let w = TimeScale.durationToDistance(duration / rate, width);
let x = TimeScale.startTimeToDistance(start + (delay / rate));
let w = TimeScale.durationToDistance(duration / rate);
let iterationW = w * (count || 1);
let delayW = TimeScale.durationToDistance(Math.abs(delay) / rate, width);
let delayW = TimeScale.durationToDistance(Math.abs(delay) / rate);
let iterations = createNode({
parent: this.containerEl,
@ -944,9 +963,9 @@ AnimationTimeBlock.prototype = {
"class": state.type + " iterations" + (count ? "" : " infinite"),
// Individual iterations are represented by setting the size of the
// repeating linear-gradient.
"style": `left:${x}px;
width:${iterationW}px;
background-size:${Math.max(w, 2)}px 100%;`
"style": `left:${x}%;
width:${iterationW}%;
background-size:${100 / (count || 1)}% 100%;`
}
});
@ -959,11 +978,8 @@ AnimationTimeBlock.prototype = {
attributes: {
"class": "name",
"title": this.getTooltipText(state),
// Position the fast-track icon with background-position, and make space
// for the negative delay with a margin-left.
"style": "background-position:" +
(iterationW - FAST_TRACK_ICON_SIZE - negativeDelayW) +
"px center;margin-left:" + negativeDelayW + "px"
// Make space for the negative delay with a margin-left.
"style": `margin-left:${negativeDelayW}%`
},
textContent: state.name
});
@ -971,14 +987,13 @@ AnimationTimeBlock.prototype = {
// Delay.
if (delay) {
// Negative delays need to start at 0.
let delayX = TimeScale.durationToDistance(
(delay < 0 ? 0 : delay) / rate, width);
let delayX = TimeScale.durationToDistance((delay < 0 ? 0 : delay) / rate);
createNode({
parent: iterations,
attributes: {
"class": "delay" + (delay < 0 ? " negative" : ""),
"style": `left:-${delayX}px;
width:${delayW}px;`
"style": `left:-${delayX}%;
width:${delayW}%;`
}
});
}

View File

@ -32,14 +32,15 @@ add_task(function*() {
info("Make sure graduations are evenly distributed and show the right times");
[...headerEl.querySelectorAll(".time-tick")].forEach((tick, i) => {
let left = parseFloat(tick.style.left);
is(Math.round(left), Math.round(i * interval),
let expectedPos = i * interval * 100 / width;
is(Math.round(left), Math.round(expectedPos),
"Graduation " + i + " is positioned correctly");
// Note that the distancetoRelativeTime and formatTime functions are tested
// separately in xpcshell test test_timeScale.js, so we assume that they
// work here.
let formattedTime = TimeScale.formatTime(
TimeScale.distanceToRelativeTime(i * interval, width));
TimeScale.distanceToRelativeTime(expectedPos, width));
is(tick.textContent, formattedTime,
"Graduation " + i + " has the right text content");
});

View File

@ -64,7 +64,7 @@ add_task(function*() {
"animation to complete");
yield selectNode(".negative-delay", inspector);
yield reloadTab(inspector);
yield waitForOutOfBoundScrubber(timeline);
yield waitForScrubberStopped(timeline);
ok(btn.classList.contains("paused"),
"The button is in paused state once finite animations are done");
@ -92,3 +92,14 @@ function waitForOutOfBoundScrubber({win, scrubberEl}) {
check();
});
}
function waitForScrubberStopped(timeline) {
return new Promise(resolve => {
timeline.on("timeline-data-changed", function onTimelineData(e, {isMoving}) {
if (!isMoving) {
timeline.off("timeline-data-changed", onTimelineData);
resolve();
}
});
});
}

View File

@ -23,16 +23,14 @@ add_task(function*() {
info("Mousedown in the header to move the scrubber");
yield synthesizeInHeaderAndWaitForChange(timeline, 50, 1, "mousedown");
let newPos = parseInt(scrubberEl.style.left, 10);
is(newPos, 50, "The scrubber moved on mousedown");
checkScrubberIsAt(scrubberEl, timeHeaderEl, 50);
ok(playTimelineButtonEl.classList.contains("paused"),
"The timeline play button is in its paused state after mousedown");
info("Continue moving the mouse and verify that the scrubber tracks it");
yield synthesizeInHeaderAndWaitForChange(timeline, 100, 1, "mousemove");
newPos = parseInt(scrubberEl.style.left, 10);
is(newPos, 100, "The scrubber followed the mouse");
checkScrubberIsAt(scrubberEl, timeHeaderEl, 100);
ok(playTimelineButtonEl.classList.contains("paused"),
"The timeline play button is in its paused state after mousemove");
@ -40,8 +38,7 @@ add_task(function*() {
info("Release the mouse and move again and verify that the scrubber stays");
EventUtils.synthesizeMouse(timeHeaderEl, 100, 1, {type: "mouseup"}, win);
EventUtils.synthesizeMouse(timeHeaderEl, 200, 1, {type: "mousemove"}, win);
newPos = parseInt(scrubberEl.style.left, 10);
is(newPos, 100, "The scrubber stopped following the mouse");
checkScrubberIsAt(scrubberEl, timeHeaderEl, 100);
info("Try to drag the scrubber handle and check that the scrubber moves");
let onDataChanged = timeline.once("timeline-data-changed");
@ -50,8 +47,7 @@ add_task(function*() {
EventUtils.synthesizeMouse(timeHeaderEl, 0, 0, {type: "mouseup"}, win);
yield onDataChanged;
newPos = parseInt(scrubberEl.style.left, 10);
is(newPos, 0, "The scrubber stopped following the mouse");
checkScrubberIsAt(scrubberEl, timeHeaderEl, 0);
});
function* synthesizeInHeaderAndWaitForChange(timeline, x, y, type) {
@ -59,3 +55,14 @@ function* synthesizeInHeaderAndWaitForChange(timeline, x, y, type) {
EventUtils.synthesizeMouse(timeline.timeHeaderEl, x, y, {type}, timeline.win);
yield onDataChanged;
}
function getPositionPercentage(pos, headerEl) {
return pos * 100 / headerEl.offsetWidth;
}
function checkScrubberIsAt(scrubberEl, timeHeaderEl, pos) {
let newPos = Math.round(parseFloat(scrubberEl.style.left));
let expectedPos = Math.round(getPositionPercentage(pos, timeHeaderEl));
is(newPos, expectedPos,
`The scrubber is at position ${pos} (${expectedPos}%)`);
}

View File

@ -46,6 +46,5 @@ add_task(function*() {
function getIterationCountFromBackground(el) {
let backgroundSize = parseFloat(el.style.backgroundSize.split(" ")[0]);
let width = el.offsetWidth;
return Math.round(width / backgroundSize);
return Math.round(100 / backgroundSize);
}

View File

@ -9,12 +9,8 @@
add_task(function*() {
yield addTab(TEST_URL_ROOT + "doc_simple_animation.html");
let {panel, controller, inspector} = yield openAnimationInspector();
let ui = yield openAnimationInspector();
yield testDataUpdates(ui);
});
function* testDataUpdates({panel, controller, inspector}) {
info("Select the test node");
yield selectNode(".animated", inspector);
@ -28,15 +24,14 @@ function* testDataUpdates({panel, controller, inspector}) {
// 45s delay + (300 * 5.5)s duration
let expectedTotalDuration = 1695 * 1000;
let timeRatio = expectedTotalDuration / timeBlockEl.offsetWidth;
// XXX: the nb and size of each iteration cannot be tested easily (displayed
// using a linear-gradient background and capped at 2px wide). They should
// be tested in bug 1173761.
let delayWidth = parseFloat(timeBlockEl.querySelector(".delay").style.width);
is(Math.round(delayWidth * timeRatio), 45 * 1000,
is(Math.round(delayWidth * expectedTotalDuration / 100), 45 * 1000,
"The timeline has the right delay");
}
});
function* setStyle(animation, panel, name, value) {
info("Change the animation style via the content DOM. Setting " +

View File

@ -58,58 +58,46 @@ const TEST_ANIMATIONS = [{
const TEST_STARTTIME_TO_DISTANCE = [{
time: 50,
width: 100,
expectedDistance: 0
}, {
time: 50,
width: 0,
expectedDistance: 0
}, {
time: 3050,
width: 200,
expectedDistance: 200
expectedDistance: 100
}, {
time: 1550,
width: 200,
expectedDistance: 100
expectedDistance: 50
}];
const TEST_DURATION_TO_DISTANCE = [{
time: 3000,
width: 100,
expectedDistance: 100
}, {
time: 0,
width: 100,
expectedDistance: 0
}];
const TEST_DISTANCE_TO_TIME = [{
distance: 100,
width: 100,
expectedTime: 3050
}, {
distance: 0,
width: 100,
expectedTime: 50
}, {
distance: 25,
width: 200,
expectedTime: 425
expectedTime: 800
}];
const TEST_DISTANCE_TO_RELATIVE_TIME = [{
distance: 100,
width: 100,
expectedTime: 3000
}, {
distance: 0,
width: 100,
expectedTime: 0
}, {
distance: 25,
width: 200,
expectedTime: 375
expectedTime: 750
}];
const TEST_FORMAT_TIME_MS = [{
@ -174,26 +162,26 @@ function run_test() {
}
do_print("Test converting start times to distances");
for (let {time, width, expectedDistance} of TEST_STARTTIME_TO_DISTANCE) {
let distance = TimeScale.startTimeToDistance(time, width);
for (let {time, expectedDistance} of TEST_STARTTIME_TO_DISTANCE) {
let distance = TimeScale.startTimeToDistance(time);
equal(distance, expectedDistance);
}
do_print("Test converting durations to distances");
for (let {time, width, expectedDistance} of TEST_DURATION_TO_DISTANCE) {
let distance = TimeScale.durationToDistance(time, width);
for (let {time, expectedDistance} of TEST_DURATION_TO_DISTANCE) {
let distance = TimeScale.durationToDistance(time);
equal(distance, expectedDistance);
}
do_print("Test converting distances to times");
for (let {distance, width, expectedTime} of TEST_DISTANCE_TO_TIME) {
let time = TimeScale.distanceToTime(distance, width);
for (let {distance, expectedTime} of TEST_DISTANCE_TO_TIME) {
let time = TimeScale.distanceToTime(distance);
equal(time, expectedTime);
}
do_print("Test converting distances to relative times");
for (let {distance, width, expectedTime} of TEST_DISTANCE_TO_RELATIVE_TIME) {
let time = TimeScale.distanceToRelativeTime(distance, width);
for (let {distance, expectedTime} of TEST_DISTANCE_TO_RELATIVE_TIME) {
let time = TimeScale.distanceToRelativeTime(distance);
equal(time, expectedTime);
}

View File

@ -165,21 +165,30 @@ body {
This is done so that the background can be built dynamically from script */
background-image: -moz-element(#time-graduations);
background-repeat: repeat-y;
/* The animations are drawn 150px from the left edge so that animated nodes
can be displayed in a sidebar */
/* Make the background be 100% of the timeline area so that it resizes with
it*/
background-size: calc(100% - var(--timeline-sidebar-width)) 100%;
background-position: var(--timeline-sidebar-width) 0;
display: flex;
flex-direction: column;
}
.animation-timeline .scrubber-wrapper {
position: absolute;
top: 0;
bottom: 0;
left: var(--timeline-sidebar-width);
right: 0;
z-index: 1;
pointer-events: none;
}
.animation-timeline .scrubber {
position: absolute;
height: 100%;
width: var(--timeline-sidebar-width);
width: 0;
border-right: 1px solid red;
box-sizing: border-box;
z-index: 1;
pointer-events: none;
}
.animation-timeline .scrubber::before {
@ -342,6 +351,7 @@ body {
/* Animations running on the compositor have the fast-track background image*/
background-image: url("images/animation-fast-track.svg");
background-repeat: no-repeat;
background-position: calc(100% - 5px) center;
}
.animation-timeline .animation .delay {