Bug 1511710 - The timeline should let you zoom. r=bhackett
authorJason Laster <jlaster@mozilla.com>
Sun, 02 Dec 2018 08:02:20 -0500
changeset 449202 037421492f0b064a57b8132deb6bdd0bb6d922d5
parent 449201 e8396abdafe1786bc1434daeaca60cb3090e6d2f
child 449203 b40559c21959c884c4ef1a23d036cfafed890606
push id35149
push userebalazs@mozilla.com
push dateMon, 03 Dec 2018 09:31:09 +0000
treeherdermozilla-central@01d0813d8203 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersbhackett
bugs1511710
milestone65.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1511710 - The timeline should let you zoom. r=bhackett Tags: Bug #: 1511710 Differential Revision: https://phabricator.services.mozilla.com/D13643
devtools/client/themes/toolbox.css
devtools/client/webreplay/components/WebReplayPlayer.js
--- a/devtools/client/themes/toolbox.css
+++ b/devtools/client/themes/toolbox.css
@@ -478,31 +478,32 @@
 
 .webreplay-player .progressBar {
   position: relative;
   width: 100%;
   height: 20px;
   background: #fff;
   margin: 4px 10px 4px 0;
   border: 1px solid #bfc9d2;
+  overflow: hidden;
 }
 
 .webreplay-player .progress {
   position: absolute;
   width: 100%;
   height: 100%;
   background: var(--progress-playing-background);
   transition-duration: var(--progress-bar-transition);
 }
 
 .webreplay-player #overlay:not(.recording) .progress::after {
   background: var(--purple-50);
   width: 1px;
   height: 100%;
-  right: 0;
+  right: -0.5px;
   opacity: 0.4;
   display: block;
   content: "";
   position: absolute;
 }
 
 .webreplay-player .recording .progress {
   background: var(--progress-recording-background);
@@ -624,8 +625,60 @@
 .webreplay-player .progress-line.end {
   opacity: 0.3;
 }
 
 .webreplay-player .recording .progress-line {
   background: #d0021b;
   opacity: 0.3;
 }
+
+.webreplay-player .tick {
+  position: absolute;
+  height: 100%;
+  transition-duration: var(--progress-bar-transition);
+}
+
+.webreplay-player .tick::before,
+.webreplay-player .tick::after {
+  height: 1.5px;
+  width: 1px;
+  right: 0;
+  position: absolute;
+  content: "";
+  display: block;
+}
+
+.webreplay-player .recording .tick::before,
+.webreplay-player .recording .tick::after {
+  background: #d0021b;
+}
+
+.webreplay-player .tick.future::before,
+.webreplay-player .tick.future::after {
+  background: #bfc9d2;
+}
+
+.webreplay-player .tick::before,
+.webreplay-player .tick::after {
+  background: var(--blue-50);
+}
+
+.webreplay-player .tick::after {
+  bottom: 0;
+}
+
+.webreplay-player .tick::before {
+  top: 0;
+}
+
+
+.webreplay-player #overlay:hover .tick {
+  opacity: 1;
+}
+
+.webreplay-player #overlay .tick {
+  opacity: 0.5;
+}
+
+.webreplay-player #overlay .tick:hover ~ .tick {
+ opacity: 0.5; 
+}
\ No newline at end of file
--- a/devtools/client/webreplay/components/WebReplayPlayer.js
+++ b/devtools/client/webreplay/components/WebReplayPlayer.js
@@ -2,17 +2,17 @@
  * 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 { Component } = require("devtools/client/shared/vendor/react");
 const ReactDOM = require("devtools/client/shared/vendor/react-dom");
 const dom = require("devtools/client/shared/vendor/react-dom-factories");
 const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
-const {sortBy} = require("devtools/client/shared/vendor/lodash");
+const { sortBy, range } = require("devtools/client/shared/vendor/lodash");
 
 const { LocalizationHelper } = require("devtools/shared/l10n");
 const L10N = new LocalizationHelper(
   "devtools/client/locales/toolbox.properties"
 );
 const getFormatStr = (key, a) => L10N.getFormatStr(`toolbox.replay.${key}`, a);
 
 const { div } = dom;
@@ -99,18 +99,21 @@ class WebReplayPlayer extends Component 
     this.state = {
       executionPoint: null,
       recordingEndpoint: null,
       seeking: false,
       recording: true,
       paused: false,
       messages: [],
       highlightedMessage: null,
+      start: 0,
+      end: 1,
     };
-    this.overlayWidth = 0;
+    this.overlayWidth = 1;
+    this.onClickProgressBar = this.onClickProgressBar.bind(this);
   }
 
   componentDidMount() {
     this.overlayWidth = this.updateOverlayWidth();
     this.threadClient.addListener("paused", this.onPaused.bind(this));
     this.threadClient.addListener("resumed", this.onResumed.bind(this));
     this.threadClient.addListener("progress", this.onProgress.bind(this));
 
@@ -154,16 +157,29 @@ class WebReplayPlayer extends Component 
   isPaused() {
     return this.state.paused;
   }
 
   isSeeking() {
     return this.state.seeking;
   }
 
+  getTickSize() {
+    const {start, end} = this.state;
+    const minSize = 10;
+
+    if (!start && !end) {
+      return minSize;
+    }
+
+    const maxSize = this.overlayWidth / 10;
+    const ratio = end - start;
+    return ((1 - ratio) * maxSize) + minSize;
+  }
+
   onPaused(_, packet) {
     if (packet && packet.recordingEndpoint) {
       const { executionPoint, recordingEndpoint } = packet;
       const closestMessage = getClosestMessage(this.state.messages, executionPoint);
 
       this.setState({
         executionPoint,
         recordingEndpoint,
@@ -199,35 +215,60 @@ class WebReplayPlayer extends Component 
 
     this.setState(newState);
   }
 
   onConsoleUpdate(consoleState) {
     const {
       messages: { visibleMessages, messagesById },
     } = consoleState;
-    const messages = visibleMessages.map(id => messagesById.get(id));
 
     if (visibleMessages != this.state.visibleMessages) {
+      const messages = sortBy(
+        visibleMessages.map(id => messagesById.get(id)),
+        message => getMessageProgress(message)
+      );
+
       this.setState({ messages, visibleMessages });
     }
   }
 
   onConsoleMessageHover(type, message) {
     if (type == "mouseleave") {
       return this.setState({ highlightedMessage: null });
     }
 
     if (type == "mouseenter") {
       return this.setState({ highlightedMessage: message.id });
     }
 
     return null;
   }
 
+  onClickProgressBar(e) {
+    if (!e.altKey) {
+      return;
+    }
+
+    const {start, end} = this.state;
+
+    const direction = e.shiftKey ? "end" : "start";
+    const { left, width } = e.currentTarget.getBoundingClientRect();
+    const clickLeft = e.clientX;
+
+    const clickPosition = (clickLeft - left) / width;
+    const position = ((end - start) * clickPosition) + start;
+
+    this.setTimelinePosition({ position, direction });
+  }
+
+  setTimelinePosition({ position, direction }) {
+    this.setState({[direction]: position});
+  }
+
   scrollToMessage() {
     const {closestMessage} = this.state;
 
     if (!closestMessage) {
       return;
     }
 
     const consoleOutput = this.console.hud.ui.outputNode;
@@ -353,38 +394,74 @@ class WebReplayPlayer extends Component 
         img: "next",
         onClick: () => this.next(),
       }),
     ];
   }
 
   updateOverlayWidth() {
     const el = ReactDOM.findDOMNode(this).querySelector(".progressBar");
-    return el.clientWidth;
+    return el ? el.clientWidth : 1;
   }
 
   // calculate pixel distance from two points
   getDistanceFrom(to, from) {
     const toPercent = this.getPercent(to);
     const fromPercent = this.getPercent(from);
 
     return ((toPercent - fromPercent) * this.overlayWidth) / 100;
   }
 
   getOffset(point) {
     const percent = this.getPercent(point);
     return (percent * this.overlayWidth) / 100;
   }
 
+  getPercent(executionPoint) {
+    const {recordingEndpoint} = this.state;
+
+    if (!recordingEndpoint) {
+      return 100;
+    }
+
+    if (!executionPoint) {
+      return 0;
+    }
+
+    const ratio = executionPoint.progress / recordingEndpoint.progress;
+    return ratio * 100;
+  }
+
+  getVisiblePercent(executionPoint) {
+    const {start, end} = this.state;
+
+    const position = this.getPercent(executionPoint) / 100;
+
+    if (position < start || position > end) {
+      return -1;
+    }
+
+    return ((position - start) / (end - start)) * 100;
+  }
+
+  getVisibleOffset(point) {
+    const percent = this.getVisiblePercent(point);
+    return (percent * this.overlayWidth) / 100;
+  }
+
   renderMessage(message, index) {
     const { messages, executionPoint, highlightedMessage } = this.state;
 
-    const offset = this.getOffset(message.executionPoint);
+    const offset = this.getVisibleOffset(message.executionPoint);
     const previousMessage = messages[index - 1];
 
+    if (offset < 0) {
+      return null;
+    }
+
     // Check to see if two messages overlay each other on the timeline
     const isOverlayed =
       previousMessage &&
       this.getDistanceFrom(
         message.executionPoint,
         previousMessage.executionPoint
       ) < markerWidth;
 
@@ -397,71 +474,91 @@ class WebReplayPlayer extends Component 
 
     return dom.a({
       className: classname("message", {
         overlayed: isOverlayed,
         future: isFuture,
         highlighted: isHighlighted,
       }),
       style: {
-        left: `${offset - markerWidth / 2}px`,
+        left: `${Math.max(offset - markerWidth/2, 0)}px`,
         zIndex: `${index + 100}`,
       },
       title: getFormatStr("jumpMessage", index + 1),
-      onClick: () => this.seek(message.executionPoint),
+      onClick: (e) => {
+        e.preventDefault();
+        e.stopPropagation();
+        this.seek(message.executionPoint);
+      },
     });
   }
 
   renderMessages() {
     const messages = this.state.messages;
-    return messages.map((message, index) => this.renderMessage(message, index));
+    return messages
+      .map((message, index) => this.renderMessage(message, index));
+  }
+
+  renderTicks() {
+    const tickSize = this.getTickSize();
+    const ticks =  Math.round((this.overlayWidth) / tickSize);
+    return range(ticks).map((value, index) => this.renderTick(index));
   }
 
-  getPercent(executionPoint) {
-    if (!this.state.recordingEndpoint) {
-      return 100;
-    }
+  renderTick(index) {
+    const { executionPoint } = this.state;
+    const tickSize = this.getTickSize();
+    const offset = Math.round(this.getOffset(executionPoint));
+    const position = index * tickSize;
+    const isFuture = position > offset;
 
-    if (!executionPoint) {
-      return 0;
-    }
-
-    const ratio =
-      executionPoint.progress / this.state.recordingEndpoint.progress;
-    return ratio * 100;
+    return dom.span({
+      className: classname("tick", {
+        future: isFuture,
+      }),
+      style: {
+        left: `${position}px`,
+        width: `${tickSize}px`,
+      },
+    });
   }
 
   render() {
-    const percent = this.getPercent(this.state.executionPoint);
+    const percent = this.getVisiblePercent(this.state.executionPoint);
     const recording = this.isRecording();
     return div(
       { className: "webreplay-player" },
       div(
         {
           id: "overlay",
           className: classname("", { recording: recording, paused: !recording }),
         },
         div(
           { className: "overlay-container " },
           div({ className: "commands" }, ...this.renderCommands()),
           div(
-            { className: "progressBar" },
+            {
+              className: "progressBar",
+              onClick: this.onClickProgressBar,
+              onDoubleClick: () => this.setState({ start: 0, end: 1 }),
+            },
             div({
               className: "progress",
               style: { width: `${percent}%` },
             }),
             div({
               className: "progress-line",
               style: { width: `${percent}%` },
             }),
             div({
               className: "progress-line end",
               style: { left: `${percent}%`, width: `${100 - percent}%` },
             }),
-            ...this.renderMessages()
+            ...this.renderMessages(),
+            ...this.renderTicks()
           )
         )
       )
     );
   }
 }
 
 module.exports = WebReplayPlayer;