Home | History | Annotate | Download | only in components
      1 /*
      2  * Copyright (C) 2007, 2008 Apple Inc.  All rights reserved.
      3  * Copyright (C) 2008, 2009 Anthony Ricaud <rik (at) webkit.org>
      4  * Copyright (C) 2009 Google Inc. All rights reserved.
      5  *
      6  * Redistribution and use in source and binary forms, with or without
      7  * modification, are permitted provided that the following conditions
      8  * are met:
      9  *
     10  * 1.  Redistributions of source code must retain the above copyright
     11  *     notice, this list of conditions and the following disclaimer.
     12  * 2.  Redistributions in binary form must reproduce the above copyright
     13  *     notice, this list of conditions and the following disclaimer in the
     14  *     documentation and/or other materials provided with the distribution.
     15  * 3.  Neither the name of Apple Computer, Inc. ("Apple") nor the names of
     16  *     its contributors may be used to endorse or promote products derived
     17  *     from this software without specific prior written permission.
     18  *
     19  * THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
     20  * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
     21  * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
     22  * DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
     23  * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
     24  * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
     25  * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
     26  * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
     27  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
     28  * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
     29  */
     30 
     31 /**
     32  * @constructor
     33  */
     34 WebInspector.TimelineGrid = function()
     35 {
     36     this.element = document.createElement("div");
     37 
     38     this._dividersElement = this.element.createChild("div", "resources-dividers");
     39 
     40     this._gridHeaderElement = document.createElement("div");
     41     this._gridHeaderElement.id = "timeline-grid-header";
     42     this._eventDividersElement = this._gridHeaderElement.createChild("div", "resources-event-dividers");
     43     this._dividersLabelBarElement = this._gridHeaderElement.createChild("div", "resources-dividers-label-bar");
     44     this.element.appendChild(this._gridHeaderElement);
     45 
     46     this._leftCurtainElement = this.element.createChild("div", "timeline-cpu-curtain-left");
     47     this._rightCurtainElement = this.element.createChild("div", "timeline-cpu-curtain-right");
     48 }
     49 
     50 /**
     51  * @param {!WebInspector.TimelineGrid.Calculator} calculator
     52  * @return {!{offsets: !Array.<number>, precision: number}}
     53  */
     54 WebInspector.TimelineGrid.calculateDividerOffsets = function(calculator)
     55 {
     56     const minGridSlicePx = 64; // minimal distance between grid lines.
     57     const gridFreeZoneAtLeftPx = 50;
     58 
     59     var clientWidth = calculator.computePosition(calculator.maximumBoundary());
     60     var dividersCount = clientWidth / minGridSlicePx;
     61     var gridSliceTime = calculator.boundarySpan() / dividersCount;
     62     var pixelsPerTime = clientWidth / calculator.boundarySpan();
     63 
     64     // Align gridSliceTime to a nearest round value.
     65     // We allow spans that fit into the formula: span = (1|2|5)x10^n,
     66     // e.g.: ...  .1  .2  .5  1  2  5  10  20  50  ...
     67     // After a span has been chosen make grid lines at multiples of the span.
     68 
     69     var logGridSliceTime = Math.ceil(Math.log(gridSliceTime) / Math.LN10);
     70     gridSliceTime = Math.pow(10, logGridSliceTime);
     71     if (gridSliceTime * pixelsPerTime >= 5 * minGridSlicePx)
     72         gridSliceTime = gridSliceTime / 5;
     73     if (gridSliceTime * pixelsPerTime >= 2 * minGridSlicePx)
     74         gridSliceTime = gridSliceTime / 2;
     75 
     76     var firstDividerTime = Math.ceil((calculator.minimumBoundary() - calculator.zeroTime()) / gridSliceTime) * gridSliceTime + calculator.zeroTime();
     77     var lastDividerTime = calculator.maximumBoundary();
     78     // Add some extra space past the right boundary as the rightmost divider label text
     79     // may be partially shown rather than just pop up when a new rightmost divider gets into the view.
     80     if (calculator.paddingLeft() > 0)
     81         lastDividerTime = lastDividerTime + minGridSlicePx / pixelsPerTime;
     82     dividersCount = Math.ceil((lastDividerTime - firstDividerTime) / gridSliceTime);
     83 
     84     var skipLeftmostDividers = calculator.paddingLeft() === 0;
     85 
     86     if (!gridSliceTime)
     87         dividersCount = 0;
     88 
     89     var offsets = [];
     90     for (var i = 0; i < dividersCount; ++i) {
     91         var left = calculator.computePosition(firstDividerTime + gridSliceTime * i);
     92         if (skipLeftmostDividers && left < gridFreeZoneAtLeftPx)
     93             continue;
     94         offsets.push(firstDividerTime + gridSliceTime * i);
     95     }
     96 
     97     return {offsets: offsets, precision: Math.max(0, -Math.floor(Math.log(gridSliceTime * 1.01) / Math.LN10))};
     98 }
     99 
    100 /**
    101  * @param {!Object} canvas
    102  * @param {!WebInspector.TimelineGrid.Calculator} calculator
    103  * @param {?Array.<number>=} dividerOffsets
    104  */
    105 WebInspector.TimelineGrid.drawCanvasGrid = function(canvas, calculator, dividerOffsets)
    106 {
    107     var context = canvas.getContext("2d");
    108     context.save();
    109     var ratio = window.devicePixelRatio;
    110     context.scale(ratio, ratio);
    111     var printDeltas = !!dividerOffsets;
    112     var width = canvas.width / window.devicePixelRatio;
    113     var height = canvas.height / window.devicePixelRatio;
    114     var precision = 0;
    115     if (!dividerOffsets) {
    116         var dividersData = WebInspector.TimelineGrid.calculateDividerOffsets(calculator);
    117         dividerOffsets = dividersData.offsets;
    118         precision = dividersData.precision;
    119     }
    120 
    121     context.fillStyle = "rgba(255, 255, 255, 0.5)";
    122     context.fillRect(0, 0, width, 15);
    123 
    124     context.fillStyle = "#333";
    125     context.strokeStyle = "rgba(0, 0, 0, 0.1)";
    126     context.textBaseline = "hanging";
    127     context.font = (printDeltas ? "italic bold 11px " : " 11px ") + WebInspector.fontFamily();
    128     context.lineWidth = 1;
    129 
    130     context.translate(0.5, 0.5);
    131     const minWidthForTitle = 60;
    132     var lastPosition = 0;
    133     var time = 0;
    134     var lastTime = 0;
    135     var paddingRight = 4;
    136     var paddingTop = 3;
    137     for (var i = 0; i < dividerOffsets.length; ++i) {
    138         time = dividerOffsets[i];
    139         var position = calculator.computePosition(time);
    140         context.beginPath();
    141         if (position - lastPosition > minWidthForTitle) {
    142             if (!printDeltas || i !== 0) {
    143                 var text = printDeltas ? calculator.formatTime(calculator.zeroTime() + time - lastTime) : calculator.formatTime(time, precision);
    144                 var textWidth = context.measureText(text).width;
    145                 var textPosition = printDeltas ? (position + lastPosition - textWidth) / 2 : position - textWidth - paddingRight;
    146                 context.fillText(text, textPosition, paddingTop);
    147             }
    148         }
    149         context.moveTo(position, 0);
    150         context.lineTo(position, height);
    151         context.stroke();
    152         lastTime = time;
    153         lastPosition = position;
    154     }
    155     context.restore();
    156 },
    157 
    158 WebInspector.TimelineGrid.prototype = {
    159     get dividersElement()
    160     {
    161         return this._dividersElement;
    162     },
    163 
    164     get dividersLabelBarElement()
    165     {
    166         return this._dividersLabelBarElement;
    167     },
    168 
    169     removeDividers: function()
    170     {
    171         this._dividersElement.removeChildren();
    172         this._dividersLabelBarElement.removeChildren();
    173     },
    174 
    175     /**
    176      * @param {!WebInspector.TimelineGrid.Calculator} calculator
    177      * @param {?Array.<number>=} dividerOffsets
    178      * @param {boolean=} printDeltas
    179      * @return {boolean}
    180      */
    181     updateDividers: function(calculator, dividerOffsets, printDeltas)
    182     {
    183         var precision = 0;
    184         if (!dividerOffsets) {
    185             var dividersData = WebInspector.TimelineGrid.calculateDividerOffsets(calculator);
    186             dividerOffsets = dividersData.offsets;
    187             precision = dividersData.precision;
    188             printDeltas = false;
    189         }
    190 
    191         var dividersElementClientWidth = this._dividersElement.clientWidth;
    192 
    193         // Reuse divider elements and labels.
    194         var divider = /** @type {?Element} */ (this._dividersElement.firstChild);
    195         var dividerLabelBar = /** @type {?Element} */ (this._dividersLabelBarElement.firstChild);
    196 
    197         const minWidthForTitle = 60;
    198         var lastPosition = 0;
    199         var lastTime = 0;
    200         for (var i = 0; i < dividerOffsets.length; ++i) {
    201             if (!divider) {
    202                 divider = document.createElement("div");
    203                 divider.className = "resources-divider";
    204                 this._dividersElement.appendChild(divider);
    205 
    206                 dividerLabelBar = document.createElement("div");
    207                 dividerLabelBar.className = "resources-divider";
    208                 var label = document.createElement("div");
    209                 label.className = "resources-divider-label";
    210                 dividerLabelBar._labelElement = label;
    211                 dividerLabelBar.appendChild(label);
    212                 this._dividersLabelBarElement.appendChild(dividerLabelBar);
    213             }
    214 
    215             var time = dividerOffsets[i];
    216             var position = calculator.computePosition(time);
    217             if (position - lastPosition > minWidthForTitle)
    218                 dividerLabelBar._labelElement.textContent = printDeltas ? calculator.formatTime(time - lastTime) : calculator.formatTime(time, precision);
    219             else
    220                 dividerLabelBar._labelElement.textContent = "";
    221 
    222             if (printDeltas)
    223                 dividerLabelBar._labelElement.style.width = Math.ceil(position - lastPosition) + "px";
    224             else
    225                 dividerLabelBar._labelElement.style.removeProperty("width");
    226 
    227             lastPosition = position;
    228             lastTime = time;
    229             var percentLeft = 100 * position / dividersElementClientWidth;
    230             divider.style.left = percentLeft + "%";
    231             dividerLabelBar.style.left = percentLeft + "%";
    232 
    233             divider = /** @type {?Element} */ (divider.nextSibling);
    234             dividerLabelBar = /** @type {?Element} */ (dividerLabelBar.nextSibling);
    235         }
    236 
    237         // Remove extras.
    238         while (divider) {
    239             var nextDivider = divider.nextSibling;
    240             this._dividersElement.removeChild(divider);
    241             divider = nextDivider;
    242         }
    243         while (dividerLabelBar) {
    244             var nextDivider = dividerLabelBar.nextSibling;
    245             this._dividersLabelBarElement.removeChild(dividerLabelBar);
    246             dividerLabelBar = nextDivider;
    247         }
    248         return true;
    249     },
    250 
    251     addEventDivider: function(divider)
    252     {
    253         this._eventDividersElement.appendChild(divider);
    254     },
    255 
    256     addEventDividers: function(dividers)
    257     {
    258         this._gridHeaderElement.removeChild(this._eventDividersElement);
    259         for (var i = 0; i < dividers.length; ++i) {
    260             if (dividers[i])
    261                 this._eventDividersElement.appendChild(dividers[i]);
    262         }
    263         this._gridHeaderElement.appendChild(this._eventDividersElement);
    264     },
    265 
    266     removeEventDividers: function()
    267     {
    268         this._eventDividersElement.removeChildren();
    269     },
    270 
    271     hideEventDividers: function()
    272     {
    273         this._eventDividersElement.classList.add("hidden");
    274     },
    275 
    276     showEventDividers: function()
    277     {
    278         this._eventDividersElement.classList.remove("hidden");
    279     },
    280 
    281     hideDividers: function()
    282     {
    283         this._dividersElement.classList.add("hidden");
    284     },
    285 
    286     showDividers: function()
    287     {
    288         this._dividersElement.classList.remove("hidden");
    289     },
    290 
    291     hideCurtains: function()
    292     {
    293         this._leftCurtainElement.classList.add("hidden");
    294         this._rightCurtainElement.classList.add("hidden");
    295     },
    296 
    297     /**
    298      * @param {number} gapOffset
    299      * @param {number} gapWidth
    300      */
    301     showCurtains: function(gapOffset, gapWidth)
    302     {
    303         this._leftCurtainElement.style.width = gapOffset + "px";
    304         this._leftCurtainElement.classList.remove("hidden");
    305         this._rightCurtainElement.style.left = (gapOffset + gapWidth) + "px";
    306         this._rightCurtainElement.classList.remove("hidden");
    307     },
    308 
    309     setScrollAndDividerTop: function(scrollTop, dividersTop)
    310     {
    311         this._dividersLabelBarElement.style.top = scrollTop + "px";
    312         this._eventDividersElement.style.top = scrollTop + "px";
    313         this._leftCurtainElement.style.top = scrollTop + "px";
    314         this._rightCurtainElement.style.top = scrollTop + "px";
    315     }
    316 }
    317 
    318 /**
    319  * @interface
    320  */
    321 WebInspector.TimelineGrid.Calculator = function() { }
    322 
    323 WebInspector.TimelineGrid.Calculator.prototype = {
    324     /**
    325      * @return {number}
    326      */
    327     paddingLeft: function() { },
    328 
    329     /**
    330      * @param {number} time
    331      * @return {number}
    332      */
    333     computePosition: function(time) { },
    334 
    335     /**
    336      * @param {number} time
    337      * @param {number=} precision
    338      * @return {string}
    339      */
    340     formatTime: function(time, precision) { },
    341 
    342     /** @return {number} */
    343     minimumBoundary: function() { },
    344 
    345     /** @return {number} */
    346     zeroTime: function() { },
    347 
    348     /** @return {number} */
    349     maximumBoundary: function() { },
    350 
    351     /** @return {number} */
    352     boundarySpan: function() { }
    353 }
    354