e66f1f5edfd9bd7515392a52cc2553ddf44d0064
[WebKit-https.git] / Source / WebInspectorUI / UserInterface / Views / TimelineRuler.js
1 /*
2  * Copyright (C) 2013 Apple Inc. All rights reserved.
3  *
4  * Redistribution and use in source and binary forms, with or without
5  * modification, are permitted provided that the following conditions
6  * are met:
7  * 1. Redistributions of source code must retain the above copyright
8  *    notice, this list of conditions and the following disclaimer.
9  * 2. Redistributions in binary form must reproduce the above copyright
10  *    notice, this list of conditions and the following disclaimer in the
11  *    documentation and/or other materials provided with the distribution.
12  *
13  * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
14  * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
15  * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
16  * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
17  * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
18  * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
19  * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
20  * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
21  * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
22  * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
23  * THE POSSIBILITY OF SUCH DAMAGE.
24  */
25
26 WebInspector.TimelineRuler = class TimelineRuler extends WebInspector.Object
27 {
28     constructor()
29     {
30         super();
31
32         this._element = document.createElement("div");
33         this._element.classList.add("timeline-ruler");
34
35         this._headerElement = document.createElement("div");
36         this._headerElement.classList.add("header");
37         this._element.appendChild(this._headerElement);
38
39         this._markersElement = document.createElement("div");
40         this._markersElement.classList.add("markers");
41         this._element.appendChild(this._markersElement);
42
43         this._zeroTime = 0;
44         this._startTime = 0;
45         this._endTime = 0;
46         this._duration = NaN;
47         this._secondsPerPixel = 0;
48         this._selectionStartTime = 0;
49         this._selectionEndTime = Infinity;
50         this._endTimePinned = false;
51         this._allowsClippedLabels = false;
52         this._allowsTimeRangeSelection = false;
53         this._minimumSelectionDuration = 0.01;
54         this._formatLabelCallback = null;
55         this._suppressNextTimeRangeSelectionChangedEvent = false;
56         this._timeRangeSelectionChanged = false;
57
58         this._markerElementMap = new Map;
59     }
60
61     // Public
62
63     get element()
64     {
65         return this._element;
66     }
67
68     get allowsClippedLabels()
69     {
70         return this._allowsClippedLabels;
71     }
72
73     set allowsClippedLabels(x)
74     {
75         if (this._allowsClippedLabels === x)
76             return;
77
78         this._allowsClippedLabels = x || false;
79
80         this._needsLayout();
81     }
82
83     set formatLabelCallback(x)
84     {
85         console.assert(typeof x === "function" || !x, x);
86
87         if (this._formatLabelCallback === x)
88             return;
89
90         this._formatLabelCallback = x || null;
91
92         this._needsLayout();
93     }
94
95     get allowsTimeRangeSelection()
96     {
97         return this._allowsTimeRangeSelection;
98     }
99
100     set allowsTimeRangeSelection(x)
101     {
102         if (this._allowsTimeRangeSelection === x)
103             return;
104
105         this._allowsTimeRangeSelection = x || false;
106
107         if (x) {
108             this._mouseDownEventListener = this._handleMouseDown.bind(this);
109             this._element.addEventListener("mousedown", this._mouseDownEventListener);
110
111             this._leftShadedAreaElement = document.createElement("div");
112             this._leftShadedAreaElement.classList.add("shaded-area");
113             this._leftShadedAreaElement.classList.add("left");
114
115             this._rightShadedAreaElement = document.createElement("div");
116             this._rightShadedAreaElement.classList.add("shaded-area");
117             this._rightShadedAreaElement.classList.add("right");
118
119             this._leftSelectionHandleElement = document.createElement("div");
120             this._leftSelectionHandleElement.classList.add("selection-handle");
121             this._leftSelectionHandleElement.classList.add("left");
122             this._leftSelectionHandleElement.addEventListener("mousedown", this._handleSelectionHandleMouseDown.bind(this));
123
124             this._rightSelectionHandleElement = document.createElement("div");
125             this._rightSelectionHandleElement.classList.add("selection-handle");
126             this._rightSelectionHandleElement.classList.add("right");
127             this._rightSelectionHandleElement.addEventListener("mousedown", this._handleSelectionHandleMouseDown.bind(this));
128
129             this._selectionDragElement = document.createElement("div");
130             this._selectionDragElement.classList.add("selection-drag");
131
132             this._needsSelectionLayout();
133         } else {
134             this._element.removeEventListener("mousedown", this._mouseDownEventListener);
135             delete this._mouseDownEventListener;
136
137             this._leftShadedAreaElement.remove();
138             this._rightShadedAreaElement.remove();
139             this._leftSelectionHandleElement.remove();
140             this._rightSelectionHandleElement.remove();
141             this._selectionDragElement.remove();
142
143             delete this._leftShadedAreaElement;
144             delete this._rightShadedAreaElement;
145             delete this._leftSelectionHandleElement;
146             delete this._rightSelectionHandleElement;
147             delete this._selectionDragElement;
148         }
149     }
150
151     get minimumSelectionDuration()
152     {
153         return this._minimumSelectionDuration;
154     }
155
156     set minimumSelectionDuration(x)
157     {
158         this._minimumSelectionDuration = x;
159     }
160
161     get zeroTime()
162     {
163         return this._zeroTime;
164     }
165
166     set zeroTime(x)
167     {
168         if (this._zeroTime === x)
169             return;
170
171         this._zeroTime = x || 0;
172
173         this._needsLayout();
174     }
175
176     get startTime()
177     {
178         return this._startTime;
179     }
180
181     set startTime(x)
182     {
183         if (this._startTime === x)
184             return;
185
186         this._startTime = x || 0;
187
188         if (!isNaN(this._duration))
189             this._endTime = this._startTime + this._duration;
190
191         this._needsLayout();
192     }
193
194     get duration()
195     {
196         if (!isNaN(this._duration))
197             return this._duration;
198         return this.endTime - this.startTime;
199     }
200
201     set duration(x)
202     {
203         if (this._duration === x)
204             return;
205
206         this._duration = x || NaN;
207
208         if (!isNaN(this._duration)) {
209             this._endTime = this._startTime + this._duration;
210             this._endTimePinned = true;
211         } else
212             this._endTimePinned = false;
213
214         this._needsLayout();
215     }
216
217     get endTime()
218     {
219         if (!this._endTimePinned && this._scheduledLayoutUpdateIdentifier)
220             this._recalculate();
221         return this._endTime;
222     }
223
224     set endTime(x)
225     {
226         if (this._endTime === x)
227             return;
228
229         this._endTime = x || 0;
230         this._endTimePinned = true;
231
232         this._needsLayout();
233     }
234
235     get secondsPerPixel()
236     {
237         if (this._scheduledLayoutUpdateIdentifier)
238             this._recalculate();
239         return this._secondsPerPixel;
240     }
241
242     set secondsPerPixel(x)
243     {
244         if (this._secondsPerPixel === x)
245             return;
246
247         this._secondsPerPixel = x || 0;
248         this._endTimePinned = false;
249         this._currentSliceTime = 0;
250
251         this._needsLayout();
252     }
253
254     get snapInterval()
255     {
256         return this._snapInterval;
257     }
258
259     set snapInterval(x)
260     {
261         if (this._snapInterval === x)
262             return;
263
264         this._snapInterval = x;
265     }
266
267     get selectionStartTime()
268     {
269         return this._selectionStartTime;
270     }
271
272     set selectionStartTime(x)
273     {
274         x = this._snapValue(x);
275         if (this._selectionStartTime === x)
276             return;
277
278         this._selectionStartTime = x || 0;
279         this._timeRangeSelectionChanged = true;
280
281         this._needsSelectionLayout();
282     }
283
284     get selectionEndTime()
285     {
286         return this._selectionEndTime;
287     }
288
289     set selectionEndTime(x)
290     {
291         x = this._snapValue(x);
292         if (this._selectionEndTime === x)
293             return;
294
295         this._selectionEndTime = x || 0;
296         this._timeRangeSelectionChanged = true;
297
298         this._needsSelectionLayout();
299     }
300
301     addMarker(marker)
302     {
303         console.assert(marker instanceof WebInspector.TimelineMarker);
304
305         if (this._markerElementMap.has(marker))
306             return;
307
308         marker.addEventListener(WebInspector.TimelineMarker.Event.TimeChanged, this._timelineMarkerTimeChanged, this);
309
310         var markerElement = document.createElement("div");
311         markerElement.classList.add(marker.type, "marker");
312
313         this._markerElementMap.set(marker, markerElement);
314
315         this._needsMarkerLayout();
316     }
317
318     elementForMarker(marker)
319     {
320         return this._markerElementMap.get(marker) || null;
321     }
322
323     updateLayout()
324     {
325         if (this._scheduledLayoutUpdateIdentifier) {
326             cancelAnimationFrame(this._scheduledLayoutUpdateIdentifier);
327             delete this._scheduledLayoutUpdateIdentifier;
328         }
329
330         var visibleWidth = this._recalculate();
331         if (visibleWidth <= 0)
332             return;
333
334         var duration = this.duration;
335
336         var pixelsPerSecond = visibleWidth / duration;
337
338         // Calculate a divider count based on the maximum allowed divider density.
339         var dividerCount = Math.round(visibleWidth / WebInspector.TimelineRuler.MinimumDividerSpacing);
340
341         if (this._endTimePinned || !this._currentSliceTime) {
342             // Calculate the slice time based on the rough divider count and the time span.
343             var sliceTime = duration / dividerCount;
344
345             // Snap the slice time to a nearest number (e.g. 0.1, 0.2, 0.5, 1, 2, 5, 10, 20, 50, etc.)
346             sliceTime = Math.pow(10, Math.ceil(Math.log(sliceTime) / Math.LN10));
347             if (sliceTime * pixelsPerSecond >= 5 * WebInspector.TimelineRuler.MinimumDividerSpacing)
348                 sliceTime = sliceTime / 5;
349             if (sliceTime * pixelsPerSecond >= 2 * WebInspector.TimelineRuler.MinimumDividerSpacing)
350                 sliceTime = sliceTime / 2;
351
352             this._currentSliceTime = sliceTime;
353         } else {
354             // Reuse the last slice time since the time duration does not scale to fit when the end time isn't pinned.
355             var sliceTime = this._currentSliceTime;
356         }
357
358         var firstDividerTime = (Math.ceil((this._startTime - this._zeroTime) / sliceTime) * sliceTime) + this._zeroTime;
359         var lastDividerTime = this._endTime;
360
361         // Calculate the divider count now based on the final slice time.
362         dividerCount = Math.ceil((lastDividerTime - firstDividerTime) / sliceTime);
363
364         // Make an extra divider in case the last one is partially visible.
365         if (!this._endTimePinned)
366             ++dividerCount;
367
368         var dividerData = {
369             count: dividerCount,
370             firstTime: firstDividerTime,
371             lastTime: lastDividerTime,
372         };
373
374         if (Object.shallowEqual(dividerData, this._currentDividers))
375             return;
376         this._currentDividers = dividerData;
377
378         var markerDividers = this._markersElement.querySelectorAll("." + WebInspector.TimelineRuler.DividerElementStyleClassName);
379
380         var dividerElement = this._headerElement.firstChild;
381
382         for (var i = 0; i <= dividerCount; ++i) {
383             if (!dividerElement) {
384                 dividerElement = document.createElement("div");
385                 dividerElement.className = WebInspector.TimelineRuler.DividerElementStyleClassName;
386                 this._headerElement.appendChild(dividerElement);
387
388                 var labelElement = document.createElement("div");
389                 labelElement.className = WebInspector.TimelineRuler.DividerLabelElementStyleClassName;
390                 dividerElement.appendChild(labelElement);
391             }
392
393             var markerDividerElement = markerDividers[i];
394             if (!markerDividerElement) {
395                 markerDividerElement = document.createElement("div");
396                 markerDividerElement.className = WebInspector.TimelineRuler.DividerElementStyleClassName;
397                 this._markersElement.appendChild(markerDividerElement);
398             }
399
400             var dividerTime = firstDividerTime + (sliceTime * i);
401
402             var newLeftPosition = (dividerTime - this._startTime) / duration;
403
404             if (!this._allowsClippedLabels) {
405                 // Don't allow dividers under 0% where they will be completely hidden.
406                 if (newLeftPosition < 0)
407                     continue;
408
409                 // When over 100% it is time to stop making/updating dividers.
410                 if (newLeftPosition > 1)
411                     break;
412
413                 // Don't allow the left-most divider spacing to be so tight it clips.
414                 if ((newLeftPosition * visibleWidth) < WebInspector.TimelineRuler.MinimumLeftDividerSpacing)
415                     continue;
416             }
417
418             this._updatePositionOfElement(dividerElement, newLeftPosition, visibleWidth);
419             this._updatePositionOfElement(markerDividerElement, newLeftPosition, visibleWidth);
420
421             console.assert(dividerElement.firstChild.classList.contains(WebInspector.TimelineRuler.DividerLabelElementStyleClassName));
422
423             dividerElement.firstChild.textContent = isNaN(dividerTime) ? "" : this._formatDividerLabelText(dividerTime - this._zeroTime);
424             dividerElement = dividerElement.nextSibling;
425         }
426
427         // Remove extra dividers.
428         while (dividerElement) {
429             var nextDividerElement = dividerElement.nextSibling;
430             dividerElement.remove();
431             dividerElement = nextDividerElement;
432         }
433
434         for (; i < markerDividers.length; ++i)
435             markerDividers[i].remove();
436
437         this._updateMarkers(visibleWidth, duration);
438         this._updateSelection(visibleWidth, duration);
439     }
440
441     updateLayoutIfNeeded()
442     {
443         // If there is a main layout scheduled we can just update layout and return, since that
444         // will update markers and the selection at the same time.
445         if (this._scheduledLayoutUpdateIdentifier) {
446             this.updateLayout();
447             return;
448         }
449
450         var visibleWidth = this._element.clientWidth;
451         if (visibleWidth <= 0)
452             return;
453
454         if (this._scheduledMarkerLayoutUpdateIdentifier)
455             this._updateMarkers(visibleWidth, this.duration);
456
457         if (this._scheduledSelectionLayoutUpdateIdentifier)
458             this._updateSelection(visibleWidth, this.duration);
459     }
460
461     // Private
462
463     _needsLayout()
464     {
465         if (this._scheduledLayoutUpdateIdentifier)
466             return;
467
468         if (this._scheduledMarkerLayoutUpdateIdentifier) {
469             cancelAnimationFrame(this._scheduledMarkerLayoutUpdateIdentifier);
470             delete this._scheduledMarkerLayoutUpdateIdentifier;
471         }
472
473         if (this._scheduledSelectionLayoutUpdateIdentifier) {
474             cancelAnimationFrame(this._scheduledSelectionLayoutUpdateIdentifier);
475             delete this._scheduledSelectionLayoutUpdateIdentifier;
476         }
477
478         this._scheduledLayoutUpdateIdentifier = requestAnimationFrame(this.updateLayout.bind(this));
479     }
480
481     _needsMarkerLayout()
482     {
483         // If layout is scheduled, abort since markers will be updated when layout happens.
484         if (this._scheduledLayoutUpdateIdentifier)
485             return;
486
487         if (this._scheduledMarkerLayoutUpdateIdentifier)
488             return;
489
490         function update()
491         {
492             delete this._scheduledMarkerLayoutUpdateIdentifier;
493
494             var visibleWidth = this._element.clientWidth;
495             if (visibleWidth <= 0)
496                 return;
497
498             this._updateMarkers(visibleWidth, this.duration);
499         }
500
501         this._scheduledMarkerLayoutUpdateIdentifier = requestAnimationFrame(update.bind(this));
502     }
503
504     _needsSelectionLayout()
505     {
506         if (!this._allowsTimeRangeSelection)
507             return;
508
509         // If layout is scheduled, abort since the selection will be updated when layout happens.
510         if (this._scheduledLayoutUpdateIdentifier)
511             return;
512
513         if (this._scheduledSelectionLayoutUpdateIdentifier)
514             return;
515
516         function update()
517         {
518             delete this._scheduledSelectionLayoutUpdateIdentifier;
519
520             var visibleWidth = this._element.clientWidth;
521             if (visibleWidth <= 0)
522                 return;
523
524             this._updateSelection(visibleWidth, this.duration);
525         }
526
527         this._scheduledSelectionLayoutUpdateIdentifier = requestAnimationFrame(update.bind(this));
528     }
529
530     _recalculate()
531     {
532         var visibleWidth = this._element.clientWidth;
533         if (visibleWidth <= 0)
534             return 0;
535
536         if (this._endTimePinned)
537             var duration = this._endTime - this._startTime;
538         else
539             var duration = visibleWidth * this._secondsPerPixel;
540
541         this._secondsPerPixel = duration / visibleWidth;
542
543         if (!this._endTimePinned)
544             this._endTime = this._startTime + (visibleWidth * this._secondsPerPixel);
545
546         return visibleWidth;
547     }
548
549     _updatePositionOfElement(element, newPosition, visibleWidth, property)
550     {
551         property = property || "left";
552
553         newPosition *= this._endTimePinned ? 100 : visibleWidth;
554         newPosition = newPosition.toFixed(2);
555
556         var currentPosition = parseFloat(element.style[property]).toFixed(2);
557         if (currentPosition !== newPosition)
558             element.style[property] = newPosition + (this._endTimePinned ? "%" : "px");
559     }
560
561     _updateMarkers(visibleWidth, duration)
562     {
563         if (this._scheduledMarkerLayoutUpdateIdentifier) {
564             cancelAnimationFrame(this._scheduledMarkerLayoutUpdateIdentifier);
565             delete this._scheduledMarkerLayoutUpdateIdentifier;
566         }
567
568         this._markerElementMap.forEach(function(markerElement, marker) {
569             var newLeftPosition = (marker.time - this._startTime) / duration;
570
571             this._updatePositionOfElement(markerElement, newLeftPosition, visibleWidth);
572
573             if (!markerElement.parentNode)
574                 this._markersElement.appendChild(markerElement);
575         }, this);
576     }
577
578     _updateSelection(visibleWidth, duration)
579     {
580         if (this._scheduledSelectionLayoutUpdateIdentifier) {
581             cancelAnimationFrame(this._scheduledSelectionLayoutUpdateIdentifier);
582             delete this._scheduledSelectionLayoutUpdateIdentifier;
583         }
584
585         this._element.classList.toggle(WebInspector.TimelineRuler.AllowsTimeRangeSelectionStyleClassName, this._allowsTimeRangeSelection);
586
587         if (!this._allowsTimeRangeSelection)
588             return;
589
590         let startTimeClamped = this._selectionStartTime < this._startTime || this._selectionStartTime > this._endTime;
591         let endTimeClamped = this._selectionEndTime < this._startTime || this._selectionEndTime > this._endTime;
592
593         this.element.classList.toggle("both-handles-clamped", startTimeClamped && endTimeClamped);
594
595         let formattedStartTimeText = this._formatDividerLabelText(this._selectionStartTime);
596         let formattedEndTimeText = this._formatDividerLabelText(this._selectionEndTime);
597
598         let newLeftPosition = Number.constrain((this._selectionStartTime - this._startTime) / duration, 0, 1);
599         this._updatePositionOfElement(this._leftShadedAreaElement, newLeftPosition, visibleWidth, "width");
600         this._updatePositionOfElement(this._leftSelectionHandleElement, newLeftPosition, visibleWidth, "left");
601         this._updatePositionOfElement(this._selectionDragElement, newLeftPosition, visibleWidth, "left");
602
603         this._leftSelectionHandleElement.classList.toggle("clamped", startTimeClamped);
604         this._leftSelectionHandleElement.classList.toggle("hidden", startTimeClamped && endTimeClamped && this._selectionStartTime < this._startTime);
605         this._leftSelectionHandleElement.title = formattedStartTimeText;
606
607         let newRightPosition = 1 - Number.constrain((this._selectionEndTime - this._startTime) / duration, 0, 1);
608         this._updatePositionOfElement(this._rightShadedAreaElement, newRightPosition, visibleWidth, "width");
609         this._updatePositionOfElement(this._rightSelectionHandleElement, newRightPosition, visibleWidth, "right");
610         this._updatePositionOfElement(this._selectionDragElement, newRightPosition, visibleWidth, "right");
611
612         this._rightSelectionHandleElement.classList.toggle("clamped", endTimeClamped);
613         this._rightSelectionHandleElement.classList.toggle("hidden", startTimeClamped && endTimeClamped && this._selectionEndTime > this._endTime);
614         this._rightSelectionHandleElement.title = formattedEndTimeText;
615
616         if (!this._selectionDragElement.parentNode) {
617             this._element.appendChild(this._selectionDragElement);
618             this._element.appendChild(this._leftShadedAreaElement);
619             this._element.appendChild(this._leftSelectionHandleElement);
620             this._element.appendChild(this._rightShadedAreaElement);
621             this._element.appendChild(this._rightSelectionHandleElement);
622         }
623
624         this._dispatchTimeRangeSelectionChangedEvent();
625     }
626
627     _formatDividerLabelText(value)
628     {
629         if (this._formatLabelCallback)
630             return this._formatLabelCallback(value);
631
632         return Number.secondsToString(value, true);
633     }
634
635     _snapValue(value)
636     {
637         if (!value || !this.snapInterval)
638             return value;
639
640         return Math.round(value / this.snapInterval) * this.snapInterval;
641     }
642
643     _dispatchTimeRangeSelectionChangedEvent()
644     {
645         if (!this._timeRangeSelectionChanged)
646             return;
647
648         if (this._suppressNextTimeRangeSelectionChangedEvent) {
649             this._suppressNextTimeRangeSelectionChangedEvent = false;
650             return;
651         }
652
653         this._timeRangeSelectionChanged = false;
654
655         this.dispatchEventToListeners(WebInspector.TimelineRuler.Event.TimeRangeSelectionChanged);
656     }
657
658     _timelineMarkerTimeChanged()
659     {
660         this._needsMarkerLayout();
661     }
662
663     _handleMouseDown(event)
664     {
665         // Only handle left mouse clicks.
666         if (event.button !== 0 || event.ctrlKey)
667             return;
668
669         this._selectionIsMove = event.target === this._selectionDragElement;
670         this._rulerBoundingClientRect = this._element.getBoundingClientRect();
671
672         if (this._selectionIsMove) {
673             this._lastMousePosition = event.pageX;
674             var selectionDragElementRect = this._selectionDragElement.getBoundingClientRect();
675             this._moveSelectionMaximumLeftOffset = this._rulerBoundingClientRect.left + (event.pageX - selectionDragElementRect.left);
676             this._moveSelectionMaximumRightOffset = this._rulerBoundingClientRect.right - (selectionDragElementRect.right - event.pageX);
677         } else
678             this._mouseDownPosition = event.pageX - this._rulerBoundingClientRect.left;
679
680         this._mouseMoveEventListener = this._handleMouseMove.bind(this);
681         this._mouseUpEventListener = this._handleMouseUp.bind(this);
682
683         // Register these listeners on the document so we can track the mouse if it leaves the ruler.
684         document.addEventListener("mousemove", this._mouseMoveEventListener);
685         document.addEventListener("mouseup", this._mouseUpEventListener);
686
687         event.preventDefault();
688         event.stopPropagation();
689     }
690
691     _handleMouseMove(event)
692     {
693         console.assert(event.button === 0);
694
695         this._suppressNextTimeRangeSelectionChangedEvent = !this._selectionIsMove;
696
697         if (this._selectionIsMove) {
698             var currentMousePosition = Math.max(this._moveSelectionMaximumLeftOffset, Math.min(this._moveSelectionMaximumRightOffset, event.pageX));
699
700             var offsetTime = (currentMousePosition - this._lastMousePosition) * this.secondsPerPixel;
701             var selectionDuration = this.selectionEndTime - this.selectionStartTime;
702             var oldSelectionStartTime = this.selectionStartTime;
703
704             this.selectionStartTime = Math.max(this.startTime, Math.min(this.selectionStartTime + offsetTime, this.endTime - selectionDuration));
705             this.selectionEndTime = this.selectionStartTime + selectionDuration;
706
707             if (this.snapInterval) {
708                 // When snapping we need to check the mouse position delta relative to the last snap, rather than the
709                 // last mouse move. If a snap occurs we adjust for the amount the cursor drifted, so that the mouse
710                 // position relative to the selection remains constant.
711                 var snapOffset = this.selectionStartTime - oldSelectionStartTime;
712                 if (!snapOffset)
713                     return;
714
715                 var positionDrift = (offsetTime - snapOffset * this.snapInterval) / this.secondsPerPixel;
716                 currentMousePosition -= positionDrift;
717             }
718
719             this._lastMousePosition = currentMousePosition;
720         } else {
721             var currentMousePosition = event.pageX - this._rulerBoundingClientRect.left;
722
723             this.selectionStartTime = Math.max(this.startTime, this.startTime + (Math.min(currentMousePosition, this._mouseDownPosition) * this.secondsPerPixel));
724             this.selectionEndTime = Math.min(this.startTime + (Math.max(currentMousePosition, this._mouseDownPosition) * this.secondsPerPixel), this.endTime);
725
726             // Turn on col-resize cursor style once dragging begins, rather than on the initial mouse down.
727             this._element.classList.add(WebInspector.TimelineRuler.ResizingSelectionStyleClassName);
728         }
729
730         this._updateSelection(this._element.clientWidth, this.duration);
731
732         event.preventDefault();
733         event.stopPropagation();
734     }
735
736     _handleMouseUp(event)
737     {
738         console.assert(event.button === 0);
739
740         if (!this._selectionIsMove) {
741             this._element.classList.remove(WebInspector.TimelineRuler.ResizingSelectionStyleClassName);
742
743             if (this.selectionEndTime - this.selectionStartTime < this.minimumSelectionDuration) {
744                 // The section is smaller than allowed, grow in the direction of the drag to meet the minumum.
745                 var currentMousePosition = event.pageX - this._rulerBoundingClientRect.left;
746                 if (currentMousePosition > this._mouseDownPosition) {
747                     this.selectionEndTime = Math.min(this.selectionStartTime + this.minimumSelectionDuration, this.endTime);
748                     this.selectionStartTime = this.selectionEndTime - this.minimumSelectionDuration;
749                 } else {
750                     this.selectionStartTime = Math.max(this.startTime, this.selectionEndTime - this.minimumSelectionDuration);
751                     this.selectionEndTime = this.selectionStartTime + this.minimumSelectionDuration;
752                 }
753             }
754         }
755
756         this._dispatchTimeRangeSelectionChangedEvent();
757
758         document.removeEventListener("mousemove", this._mouseMoveEventListener);
759         document.removeEventListener("mouseup", this._mouseUpEventListener);
760
761         delete this._mouseMovedEventListener;
762         delete this._mouseUpEventListener;
763         delete this._mouseDownPosition;
764         delete this._lastMousePosition;
765         delete this._selectionIsMove;
766         delete this._rulerBoundingClientRect;
767         delete this._moveSelectionMaximumLeftOffset;
768         delete this._moveSelectionMaximumRightOffset;
769
770         event.preventDefault();
771         event.stopPropagation();
772     }
773
774     _handleSelectionHandleMouseDown(event)
775     {
776         // Only handle left mouse clicks.
777         if (event.button !== 0 || event.ctrlKey)
778             return;
779
780         this._dragHandleIsStartTime = event.target === this._leftSelectionHandleElement;
781         this._mouseDownPosition = event.pageX - this._element.totalOffsetLeft;
782
783         this._selectionHandleMouseMoveEventListener = this._handleSelectionHandleMouseMove.bind(this);
784         this._selectionHandleMouseUpEventListener = this._handleSelectionHandleMouseUp.bind(this);
785
786         // Register these listeners on the document so we can track the mouse if it leaves the ruler.
787         document.addEventListener("mousemove", this._selectionHandleMouseMoveEventListener);
788         document.addEventListener("mouseup", this._selectionHandleMouseUpEventListener);
789
790         this._element.classList.add(WebInspector.TimelineRuler.ResizingSelectionStyleClassName);
791
792         event.preventDefault();
793         event.stopPropagation();
794     }
795
796     _handleSelectionHandleMouseMove(event)
797     {
798         console.assert(event.button === 0);
799
800         var currentMousePosition = event.pageX - this._element.totalOffsetLeft;
801         var currentTime = this.startTime + (currentMousePosition * this.secondsPerPixel);
802         if (this.snapInterval)
803             currentTime = this._snapValue(currentTime);
804
805         if (event.altKey && !event.ctrlKey && !event.metaKey && !event.shiftKey) {
806             // Resize the selection on both sides when the Option keys is held down.
807             if (this._dragHandleIsStartTime) {
808                 var timeDifference = currentTime - this.selectionStartTime;
809                 this.selectionStartTime = Math.max(this.startTime, Math.min(currentTime, this.selectionEndTime - this.minimumSelectionDuration));
810                 this.selectionEndTime = Math.min(Math.max(this.selectionStartTime + this.minimumSelectionDuration, this.selectionEndTime - timeDifference), this.endTime);
811             } else {
812                 var timeDifference = currentTime - this.selectionEndTime;
813                 this.selectionEndTime = Math.min(Math.max(this.selectionStartTime + this.minimumSelectionDuration, currentTime), this.endTime);
814                 this.selectionStartTime = Math.max(this.startTime, Math.min(this.selectionStartTime - timeDifference, this.selectionEndTime - this.minimumSelectionDuration));
815             }
816         } else {
817             // Resize the selection on side being dragged.
818             if (this._dragHandleIsStartTime)
819                 this.selectionStartTime = Math.max(this.startTime, Math.min(currentTime, this.selectionEndTime - this.minimumSelectionDuration));
820             else
821                 this.selectionEndTime = Math.min(Math.max(this.selectionStartTime + this.minimumSelectionDuration, currentTime), this.endTime);
822         }
823
824         this._updateSelection(this._element.clientWidth, this.duration);
825
826         event.preventDefault();
827         event.stopPropagation();
828     }
829
830     _handleSelectionHandleMouseUp(event)
831     {
832         console.assert(event.button === 0);
833
834         this._element.classList.remove(WebInspector.TimelineRuler.ResizingSelectionStyleClassName);
835
836         document.removeEventListener("mousemove", this._selectionHandleMouseMoveEventListener);
837         document.removeEventListener("mouseup", this._selectionHandleMouseUpEventListener);
838
839         delete this._selectionHandleMouseMoveEventListener;
840         delete this._selectionHandleMouseUpEventListener;
841         delete this._dragHandleIsStartTime;
842         delete this._mouseDownPosition;
843
844         event.preventDefault();
845         event.stopPropagation();
846     }
847 };
848
849 WebInspector.TimelineRuler.MinimumLeftDividerSpacing = 48;
850 WebInspector.TimelineRuler.MinimumDividerSpacing = 64;
851
852 WebInspector.TimelineRuler.AllowsTimeRangeSelectionStyleClassName = "allows-time-range-selection";
853 WebInspector.TimelineRuler.ResizingSelectionStyleClassName = "resizing-selection";
854 WebInspector.TimelineRuler.DividerElementStyleClassName = "divider";
855 WebInspector.TimelineRuler.DividerLabelElementStyleClassName = "label";
856
857 WebInspector.TimelineRuler.Event = {
858     TimeRangeSelectionChanged: "time-ruler-time-range-selection-changed"
859 };