Web Inspector: Timeline current time marker does not start moving when starting recor...
[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             this._updateMarkers(visibleWidth, duration);
376             this._updateSelection(visibleWidth, duration);
377             return;
378         }
379
380         this._currentDividers = dividerData;
381
382         var markerDividers = this._markersElement.querySelectorAll("." + WebInspector.TimelineRuler.DividerElementStyleClassName);
383
384         var dividerElement = this._headerElement.firstChild;
385
386         for (var i = 0; i <= dividerCount; ++i) {
387             if (!dividerElement) {
388                 dividerElement = document.createElement("div");
389                 dividerElement.className = WebInspector.TimelineRuler.DividerElementStyleClassName;
390                 this._headerElement.appendChild(dividerElement);
391
392                 var labelElement = document.createElement("div");
393                 labelElement.className = WebInspector.TimelineRuler.DividerLabelElementStyleClassName;
394                 dividerElement.appendChild(labelElement);
395             }
396
397             var markerDividerElement = markerDividers[i];
398             if (!markerDividerElement) {
399                 markerDividerElement = document.createElement("div");
400                 markerDividerElement.className = WebInspector.TimelineRuler.DividerElementStyleClassName;
401                 this._markersElement.appendChild(markerDividerElement);
402             }
403
404             var dividerTime = firstDividerTime + (sliceTime * i);
405
406             var newLeftPosition = (dividerTime - this._startTime) / duration;
407
408             if (!this._allowsClippedLabels) {
409                 // Don't allow dividers under 0% where they will be completely hidden.
410                 if (newLeftPosition < 0)
411                     continue;
412
413                 // When over 100% it is time to stop making/updating dividers.
414                 if (newLeftPosition > 1)
415                     break;
416
417                 // Don't allow the left-most divider spacing to be so tight it clips.
418                 if ((newLeftPosition * visibleWidth) < WebInspector.TimelineRuler.MinimumLeftDividerSpacing)
419                     continue;
420             }
421
422             this._updatePositionOfElement(dividerElement, newLeftPosition, visibleWidth);
423             this._updatePositionOfElement(markerDividerElement, newLeftPosition, visibleWidth);
424
425             console.assert(dividerElement.firstChild.classList.contains(WebInspector.TimelineRuler.DividerLabelElementStyleClassName));
426
427             dividerElement.firstChild.textContent = isNaN(dividerTime) ? "" : this._formatDividerLabelText(dividerTime - this._zeroTime);
428             dividerElement = dividerElement.nextSibling;
429         }
430
431         // Remove extra dividers.
432         while (dividerElement) {
433             var nextDividerElement = dividerElement.nextSibling;
434             dividerElement.remove();
435             dividerElement = nextDividerElement;
436         }
437
438         for (; i < markerDividers.length; ++i)
439             markerDividers[i].remove();
440
441         this._updateMarkers(visibleWidth, duration);
442         this._updateSelection(visibleWidth, duration);
443     }
444
445     updateLayoutIfNeeded()
446     {
447         // If there is a main layout scheduled we can just update layout and return, since that
448         // will update markers and the selection at the same time.
449         if (this._scheduledLayoutUpdateIdentifier) {
450             this.updateLayout();
451             return;
452         }
453
454         var visibleWidth = this._element.clientWidth;
455         if (visibleWidth <= 0)
456             return;
457
458         if (this._scheduledMarkerLayoutUpdateIdentifier)
459             this._updateMarkers(visibleWidth, this.duration);
460
461         if (this._scheduledSelectionLayoutUpdateIdentifier)
462             this._updateSelection(visibleWidth, this.duration);
463     }
464
465     // Private
466
467     _needsLayout()
468     {
469         if (this._scheduledLayoutUpdateIdentifier)
470             return;
471
472         if (this._scheduledMarkerLayoutUpdateIdentifier) {
473             cancelAnimationFrame(this._scheduledMarkerLayoutUpdateIdentifier);
474             delete this._scheduledMarkerLayoutUpdateIdentifier;
475         }
476
477         if (this._scheduledSelectionLayoutUpdateIdentifier) {
478             cancelAnimationFrame(this._scheduledSelectionLayoutUpdateIdentifier);
479             delete this._scheduledSelectionLayoutUpdateIdentifier;
480         }
481
482         this._scheduledLayoutUpdateIdentifier = requestAnimationFrame(this.updateLayout.bind(this));
483     }
484
485     _needsMarkerLayout()
486     {
487         // If layout is scheduled, abort since markers will be updated when layout happens.
488         if (this._scheduledLayoutUpdateIdentifier)
489             return;
490
491         if (this._scheduledMarkerLayoutUpdateIdentifier)
492             return;
493
494         function update()
495         {
496             delete this._scheduledMarkerLayoutUpdateIdentifier;
497
498             var visibleWidth = this._element.clientWidth;
499             if (visibleWidth <= 0)
500                 return;
501
502             this._updateMarkers(visibleWidth, this.duration);
503         }
504
505         this._scheduledMarkerLayoutUpdateIdentifier = requestAnimationFrame(update.bind(this));
506     }
507
508     _needsSelectionLayout()
509     {
510         if (!this._allowsTimeRangeSelection)
511             return;
512
513         // If layout is scheduled, abort since the selection will be updated when layout happens.
514         if (this._scheduledLayoutUpdateIdentifier)
515             return;
516
517         if (this._scheduledSelectionLayoutUpdateIdentifier)
518             return;
519
520         function update()
521         {
522             delete this._scheduledSelectionLayoutUpdateIdentifier;
523
524             var visibleWidth = this._element.clientWidth;
525             if (visibleWidth <= 0)
526                 return;
527
528             this._updateSelection(visibleWidth, this.duration);
529         }
530
531         this._scheduledSelectionLayoutUpdateIdentifier = requestAnimationFrame(update.bind(this));
532     }
533
534     _recalculate()
535     {
536         var visibleWidth = this._element.clientWidth;
537         if (visibleWidth <= 0)
538             return 0;
539
540         if (this._endTimePinned)
541             var duration = this._endTime - this._startTime;
542         else
543             var duration = visibleWidth * this._secondsPerPixel;
544
545         this._secondsPerPixel = duration / visibleWidth;
546
547         if (!this._endTimePinned)
548             this._endTime = this._startTime + (visibleWidth * this._secondsPerPixel);
549
550         return visibleWidth;
551     }
552
553     _updatePositionOfElement(element, newPosition, visibleWidth, property)
554     {
555         property = property || "left";
556
557         newPosition *= this._endTimePinned ? 100 : visibleWidth;
558         newPosition = newPosition.toFixed(2);
559
560         var currentPosition = parseFloat(element.style[property]).toFixed(2);
561         if (currentPosition !== newPosition)
562             element.style[property] = newPosition + (this._endTimePinned ? "%" : "px");
563     }
564
565     _updateMarkers(visibleWidth, duration)
566     {
567         if (this._scheduledMarkerLayoutUpdateIdentifier) {
568             cancelAnimationFrame(this._scheduledMarkerLayoutUpdateIdentifier);
569             delete this._scheduledMarkerLayoutUpdateIdentifier;
570         }
571
572         this._markerElementMap.forEach(function(markerElement, marker) {
573             var newLeftPosition = (marker.time - this._startTime) / duration;
574
575             this._updatePositionOfElement(markerElement, newLeftPosition, visibleWidth);
576
577             if (!markerElement.parentNode)
578                 this._markersElement.appendChild(markerElement);
579         }, this);
580     }
581
582     _updateSelection(visibleWidth, duration)
583     {
584         if (this._scheduledSelectionLayoutUpdateIdentifier) {
585             cancelAnimationFrame(this._scheduledSelectionLayoutUpdateIdentifier);
586             delete this._scheduledSelectionLayoutUpdateIdentifier;
587         }
588
589         this._element.classList.toggle(WebInspector.TimelineRuler.AllowsTimeRangeSelectionStyleClassName, this._allowsTimeRangeSelection);
590
591         if (!this._allowsTimeRangeSelection)
592             return;
593
594         let startTimeClamped = this._selectionStartTime < this._startTime || this._selectionStartTime > this._endTime;
595         let endTimeClamped = this._selectionEndTime < this._startTime || this._selectionEndTime > this._endTime;
596
597         this.element.classList.toggle("both-handles-clamped", startTimeClamped && endTimeClamped);
598
599         let formattedStartTimeText = this._formatDividerLabelText(this._selectionStartTime);
600         let formattedEndTimeText = this._formatDividerLabelText(this._selectionEndTime);
601
602         let newLeftPosition = Number.constrain((this._selectionStartTime - this._startTime) / duration, 0, 1);
603         this._updatePositionOfElement(this._leftShadedAreaElement, newLeftPosition, visibleWidth, "width");
604         this._updatePositionOfElement(this._leftSelectionHandleElement, newLeftPosition, visibleWidth, "left");
605         this._updatePositionOfElement(this._selectionDragElement, newLeftPosition, visibleWidth, "left");
606
607         this._leftSelectionHandleElement.classList.toggle("clamped", startTimeClamped);
608         this._leftSelectionHandleElement.classList.toggle("hidden", startTimeClamped && endTimeClamped && this._selectionStartTime < this._startTime);
609         this._leftSelectionHandleElement.title = formattedStartTimeText;
610
611         let newRightPosition = 1 - Number.constrain((this._selectionEndTime - this._startTime) / duration, 0, 1);
612         this._updatePositionOfElement(this._rightShadedAreaElement, newRightPosition, visibleWidth, "width");
613         this._updatePositionOfElement(this._rightSelectionHandleElement, newRightPosition, visibleWidth, "right");
614         this._updatePositionOfElement(this._selectionDragElement, newRightPosition, visibleWidth, "right");
615
616         this._rightSelectionHandleElement.classList.toggle("clamped", endTimeClamped);
617         this._rightSelectionHandleElement.classList.toggle("hidden", startTimeClamped && endTimeClamped && this._selectionEndTime > this._endTime);
618         this._rightSelectionHandleElement.title = formattedEndTimeText;
619
620         if (!this._selectionDragElement.parentNode) {
621             this._element.appendChild(this._selectionDragElement);
622             this._element.appendChild(this._leftShadedAreaElement);
623             this._element.appendChild(this._leftSelectionHandleElement);
624             this._element.appendChild(this._rightShadedAreaElement);
625             this._element.appendChild(this._rightSelectionHandleElement);
626         }
627
628         this._dispatchTimeRangeSelectionChangedEvent();
629     }
630
631     _formatDividerLabelText(value)
632     {
633         if (this._formatLabelCallback)
634             return this._formatLabelCallback(value);
635
636         return Number.secondsToString(value, true);
637     }
638
639     _snapValue(value)
640     {
641         if (!value || !this.snapInterval)
642             return value;
643
644         return Math.round(value / this.snapInterval) * this.snapInterval;
645     }
646
647     _dispatchTimeRangeSelectionChangedEvent()
648     {
649         if (!this._timeRangeSelectionChanged)
650             return;
651
652         if (this._suppressNextTimeRangeSelectionChangedEvent) {
653             this._suppressNextTimeRangeSelectionChangedEvent = false;
654             return;
655         }
656
657         this._timeRangeSelectionChanged = false;
658
659         this.dispatchEventToListeners(WebInspector.TimelineRuler.Event.TimeRangeSelectionChanged);
660     }
661
662     _timelineMarkerTimeChanged()
663     {
664         this._needsMarkerLayout();
665     }
666
667     _handleMouseDown(event)
668     {
669         // Only handle left mouse clicks.
670         if (event.button !== 0 || event.ctrlKey)
671             return;
672
673         this._selectionIsMove = event.target === this._selectionDragElement;
674         this._rulerBoundingClientRect = this._element.getBoundingClientRect();
675
676         if (this._selectionIsMove) {
677             this._lastMousePosition = event.pageX;
678             var selectionDragElementRect = this._selectionDragElement.getBoundingClientRect();
679             this._moveSelectionMaximumLeftOffset = this._rulerBoundingClientRect.left + (event.pageX - selectionDragElementRect.left);
680             this._moveSelectionMaximumRightOffset = this._rulerBoundingClientRect.right - (selectionDragElementRect.right - event.pageX);
681         } else
682             this._mouseDownPosition = event.pageX - this._rulerBoundingClientRect.left;
683
684         this._mouseMoveEventListener = this._handleMouseMove.bind(this);
685         this._mouseUpEventListener = this._handleMouseUp.bind(this);
686
687         // Register these listeners on the document so we can track the mouse if it leaves the ruler.
688         document.addEventListener("mousemove", this._mouseMoveEventListener);
689         document.addEventListener("mouseup", this._mouseUpEventListener);
690
691         event.preventDefault();
692         event.stopPropagation();
693     }
694
695     _handleMouseMove(event)
696     {
697         console.assert(event.button === 0);
698
699         this._suppressNextTimeRangeSelectionChangedEvent = !this._selectionIsMove;
700
701         if (this._selectionIsMove) {
702             var currentMousePosition = Math.max(this._moveSelectionMaximumLeftOffset, Math.min(this._moveSelectionMaximumRightOffset, event.pageX));
703
704             var offsetTime = (currentMousePosition - this._lastMousePosition) * this.secondsPerPixel;
705             var selectionDuration = this.selectionEndTime - this.selectionStartTime;
706             var oldSelectionStartTime = this.selectionStartTime;
707
708             this.selectionStartTime = Math.max(this.startTime, Math.min(this.selectionStartTime + offsetTime, this.endTime - selectionDuration));
709             this.selectionEndTime = this.selectionStartTime + selectionDuration;
710
711             if (this.snapInterval) {
712                 // When snapping we need to check the mouse position delta relative to the last snap, rather than the
713                 // last mouse move. If a snap occurs we adjust for the amount the cursor drifted, so that the mouse
714                 // position relative to the selection remains constant.
715                 var snapOffset = this.selectionStartTime - oldSelectionStartTime;
716                 if (!snapOffset)
717                     return;
718
719                 var positionDrift = (offsetTime - snapOffset * this.snapInterval) / this.secondsPerPixel;
720                 currentMousePosition -= positionDrift;
721             }
722
723             this._lastMousePosition = currentMousePosition;
724         } else {
725             var currentMousePosition = event.pageX - this._rulerBoundingClientRect.left;
726
727             this.selectionStartTime = Math.max(this.startTime, this.startTime + (Math.min(currentMousePosition, this._mouseDownPosition) * this.secondsPerPixel));
728             this.selectionEndTime = Math.min(this.startTime + (Math.max(currentMousePosition, this._mouseDownPosition) * this.secondsPerPixel), this.endTime);
729
730             // Turn on col-resize cursor style once dragging begins, rather than on the initial mouse down.
731             this._element.classList.add(WebInspector.TimelineRuler.ResizingSelectionStyleClassName);
732         }
733
734         this._updateSelection(this._element.clientWidth, this.duration);
735
736         event.preventDefault();
737         event.stopPropagation();
738     }
739
740     _handleMouseUp(event)
741     {
742         console.assert(event.button === 0);
743
744         if (!this._selectionIsMove) {
745             this._element.classList.remove(WebInspector.TimelineRuler.ResizingSelectionStyleClassName);
746
747             if (this.selectionEndTime - this.selectionStartTime < this.minimumSelectionDuration) {
748                 // The section is smaller than allowed, grow in the direction of the drag to meet the minumum.
749                 var currentMousePosition = event.pageX - this._rulerBoundingClientRect.left;
750                 if (currentMousePosition > this._mouseDownPosition) {
751                     this.selectionEndTime = Math.min(this.selectionStartTime + this.minimumSelectionDuration, this.endTime);
752                     this.selectionStartTime = this.selectionEndTime - this.minimumSelectionDuration;
753                 } else {
754                     this.selectionStartTime = Math.max(this.startTime, this.selectionEndTime - this.minimumSelectionDuration);
755                     this.selectionEndTime = this.selectionStartTime + this.minimumSelectionDuration;
756                 }
757             }
758         }
759
760         this._dispatchTimeRangeSelectionChangedEvent();
761
762         document.removeEventListener("mousemove", this._mouseMoveEventListener);
763         document.removeEventListener("mouseup", this._mouseUpEventListener);
764
765         delete this._mouseMovedEventListener;
766         delete this._mouseUpEventListener;
767         delete this._mouseDownPosition;
768         delete this._lastMousePosition;
769         delete this._selectionIsMove;
770         delete this._rulerBoundingClientRect;
771         delete this._moveSelectionMaximumLeftOffset;
772         delete this._moveSelectionMaximumRightOffset;
773
774         event.preventDefault();
775         event.stopPropagation();
776     }
777
778     _handleSelectionHandleMouseDown(event)
779     {
780         // Only handle left mouse clicks.
781         if (event.button !== 0 || event.ctrlKey)
782             return;
783
784         this._dragHandleIsStartTime = event.target === this._leftSelectionHandleElement;
785         this._mouseDownPosition = event.pageX - this._element.totalOffsetLeft;
786
787         this._selectionHandleMouseMoveEventListener = this._handleSelectionHandleMouseMove.bind(this);
788         this._selectionHandleMouseUpEventListener = this._handleSelectionHandleMouseUp.bind(this);
789
790         // Register these listeners on the document so we can track the mouse if it leaves the ruler.
791         document.addEventListener("mousemove", this._selectionHandleMouseMoveEventListener);
792         document.addEventListener("mouseup", this._selectionHandleMouseUpEventListener);
793
794         this._element.classList.add(WebInspector.TimelineRuler.ResizingSelectionStyleClassName);
795
796         event.preventDefault();
797         event.stopPropagation();
798     }
799
800     _handleSelectionHandleMouseMove(event)
801     {
802         console.assert(event.button === 0);
803
804         var currentMousePosition = event.pageX - this._element.totalOffsetLeft;
805         var currentTime = this.startTime + (currentMousePosition * this.secondsPerPixel);
806         if (this.snapInterval)
807             currentTime = this._snapValue(currentTime);
808
809         if (event.altKey && !event.ctrlKey && !event.metaKey && !event.shiftKey) {
810             // Resize the selection on both sides when the Option keys is held down.
811             if (this._dragHandleIsStartTime) {
812                 var timeDifference = currentTime - this.selectionStartTime;
813                 this.selectionStartTime = Math.max(this.startTime, Math.min(currentTime, this.selectionEndTime - this.minimumSelectionDuration));
814                 this.selectionEndTime = Math.min(Math.max(this.selectionStartTime + this.minimumSelectionDuration, this.selectionEndTime - timeDifference), this.endTime);
815             } else {
816                 var timeDifference = currentTime - this.selectionEndTime;
817                 this.selectionEndTime = Math.min(Math.max(this.selectionStartTime + this.minimumSelectionDuration, currentTime), this.endTime);
818                 this.selectionStartTime = Math.max(this.startTime, Math.min(this.selectionStartTime - timeDifference, this.selectionEndTime - this.minimumSelectionDuration));
819             }
820         } else {
821             // Resize the selection on side being dragged.
822             if (this._dragHandleIsStartTime)
823                 this.selectionStartTime = Math.max(this.startTime, Math.min(currentTime, this.selectionEndTime - this.minimumSelectionDuration));
824             else
825                 this.selectionEndTime = Math.min(Math.max(this.selectionStartTime + this.minimumSelectionDuration, currentTime), this.endTime);
826         }
827
828         this._updateSelection(this._element.clientWidth, this.duration);
829
830         event.preventDefault();
831         event.stopPropagation();
832     }
833
834     _handleSelectionHandleMouseUp(event)
835     {
836         console.assert(event.button === 0);
837
838         this._element.classList.remove(WebInspector.TimelineRuler.ResizingSelectionStyleClassName);
839
840         document.removeEventListener("mousemove", this._selectionHandleMouseMoveEventListener);
841         document.removeEventListener("mouseup", this._selectionHandleMouseUpEventListener);
842
843         delete this._selectionHandleMouseMoveEventListener;
844         delete this._selectionHandleMouseUpEventListener;
845         delete this._dragHandleIsStartTime;
846         delete this._mouseDownPosition;
847
848         event.preventDefault();
849         event.stopPropagation();
850     }
851 };
852
853 WebInspector.TimelineRuler.MinimumLeftDividerSpacing = 48;
854 WebInspector.TimelineRuler.MinimumDividerSpacing = 64;
855
856 WebInspector.TimelineRuler.AllowsTimeRangeSelectionStyleClassName = "allows-time-range-selection";
857 WebInspector.TimelineRuler.ResizingSelectionStyleClassName = "resizing-selection";
858 WebInspector.TimelineRuler.DividerElementStyleClassName = "divider";
859 WebInspector.TimelineRuler.DividerLabelElementStyleClassName = "label";
860
861 WebInspector.TimelineRuler.Event = {
862     TimeRangeSelectionChanged: "time-ruler-time-range-selection-changed"
863 };