blob: d22bc170fd1ee2db9f2d3f898bac479ad65839f8 [file] [log] [blame]
<!DOCTYPE html>
<!--
Copyright (c) 2012 The Chromium Authors. All rights reserved.
Use of this source code is governed by a BSD-style license that can be
found in the LICENSE file.
-->
<link rel="import" href="/tracing/base/event.html">
<link rel="import" href="/tracing/base/unit.html">
<link rel="import" href="/tracing/model/event_set.html">
<link rel="import" href="/tracing/ui/base/animation.html">
<link rel="import" href="/tracing/ui/base/animation_controller.html">
<link rel="import" href="/tracing/ui/base/dom_helpers.html">
<link rel="import" href="/tracing/ui/base/draw_helpers.html">
<link rel="import" href="/tracing/ui/timeline_display_transform.html">
<link rel="import" href="/tracing/ui/timeline_interest_range.html">
<link rel="import" href="/tracing/ui/tracks/container_to_track_map.html">
<link rel="import" href="/tracing/ui/tracks/event_to_track_map.html">
<script>
'use strict';
/**
* @fileoverview Code for the viewport.
*/
tr.exportTo('tr.ui', function() {
var TimelineDisplayTransform = tr.ui.TimelineDisplayTransform;
var TimelineInterestRange = tr.ui.TimelineInterestRange;
var IDEAL_MAJOR_MARK_DISTANCE_PX = 150;
// Keep 5 digits of precision when rounding the major mark distances.
var MAJOR_MARK_ROUNDING_FACTOR = 100000;
class AnimationControllerProxy {
constructor(target) {
this.target_ = target;
}
get panX() {
return this.target_.currentDisplayTransform_.panX;
}
set panX(panX) {
this.target_.currentDisplayTransform_.panX = panX;
}
get panY() {
return this.target_.currentDisplayTransform_.panY;
}
set panY(panY) {
this.target_.currentDisplayTransform_.panY = panY;
}
get scaleX() {
return this.target_.currentDisplayTransform_.scaleX;
}
set scaleX(scaleX) {
this.target_.currentDisplayTransform_.scaleX = scaleX;
}
cloneAnimationState() {
return this.target_.currentDisplayTransform_.clone();
}
xPanWorldPosToViewPos(xWorld, xView) {
this.target_.currentDisplayTransform_.xPanWorldPosToViewPos(
xWorld, xView, this.target_.modelTrackContainer_.canvas.clientWidth);
}
}
/**
* The TimelineViewport manages the transform used for navigating
* within the timeline. It is a simple transform:
* x' = (x+pan) * scale
*
* The timeline code tries to avoid directly accessing this transform,
* instead using this class to do conversion between world and viewspace,
* as well as the math for centering the viewport in various interesting
* ways.
*
* @constructor
* @extends {tr.b.EventTarget}
*/
function TimelineViewport(parentEl) {
this.parentEl_ = parentEl;
this.modelTrackContainer_ = undefined;
this.currentDisplayTransform_ = new TimelineDisplayTransform();
this.initAnimationController_();
// Flow events
this.showFlowEvents_ = false;
// Highlights.
this.highlightVSync_ = false;
// High details.
this.highDetails_ = false;
// Grid system.
this.gridTimebase_ = 0;
this.gridStep_ = 1000 / 60;
this.gridEnabled_ = false;
// Init logic.
this.hasCalledSetupFunction_ = false;
this.onResize_ = this.onResize_.bind(this);
this.onModelTrackControllerScroll_ =
this.onModelTrackControllerScroll_.bind(this);
this.timeMode_ = TimelineViewport.TimeMode.TIME_IN_MS;
// Major mark positions are where the gridlines/ruler marks are placed along
// the x-axis.
this.majorMarkWorldPositions_ = [];
this.majorMarkUnit_ = undefined;
this.interestRange_ = new TimelineInterestRange(this);
this.eventToTrackMap_ = new tr.ui.tracks.EventToTrackMap();
this.containerToTrackMap = new tr.ui.tracks.ContainerToTrackMap();
this.dispatchChangeEvent = this.dispatchChangeEvent.bind(this);
}
TimelineViewport.TimeMode = {
TIME_IN_MS: 0,
REVISIONS: 1
};
TimelineViewport.prototype = {
__proto__: tr.b.EventTarget.prototype,
/**
* @return {boolean} Whether the current timeline is attached to the
* document.
*/
get isAttachedToDocumentOrInTestMode() {
// Allow not providing a parent element, used by tests.
if (this.parentEl_ === undefined)
return;
return tr.ui.b.isElementAttachedToDocument(this.parentEl_);
},
onResize_: function() {
this.dispatchChangeEvent();
},
/**
* Fires the change event on this viewport. Used to notify listeners
* to redraw when the underlying model has been mutated.
*/
dispatchChangeEvent: function() {
tr.b.dispatchSimpleEvent(this, 'change');
},
detach: function() {
window.removeEventListener('resize', this.dispatchChangeEvent);
},
initAnimationController_: function() {
this.dtAnimationController_ = new tr.ui.b.AnimationController();
this.dtAnimationController_.addEventListener(
'didtick', function(e) {
this.onCurentDisplayTransformChange_(e.oldTargetState);
}.bind(this));
this.dtAnimationController_.target = new AnimationControllerProxy(this);
},
get currentDisplayTransform() {
return this.currentDisplayTransform_;
},
setDisplayTransformImmediately: function(displayTransform) {
this.dtAnimationController_.cancelActiveAnimation();
var oldDisplayTransform =
this.dtAnimationController_.target.cloneAnimationState();
this.currentDisplayTransform_.set(displayTransform);
this.onCurentDisplayTransformChange_(oldDisplayTransform);
},
queueDisplayTransformAnimation: function(animation) {
if (!(animation instanceof tr.ui.b.Animation))
throw new Error('animation must be instanceof tr.ui.b.Animation');
this.dtAnimationController_.queueAnimation(animation);
},
onCurentDisplayTransformChange_: function(oldDisplayTransform) {
// Ensure panY stays clamped in the track container's scroll range.
if (this.modelTrackContainer_) {
this.currentDisplayTransform.panY = tr.b.math.clamp(
this.currentDisplayTransform.panY,
0,
this.modelTrackContainer_.scrollHeight -
this.modelTrackContainer_.clientHeight);
}
var changed = !this.currentDisplayTransform.equals(oldDisplayTransform);
var yChanged = this.currentDisplayTransform.panY !==
oldDisplayTransform.panY;
if (yChanged)
this.modelTrackContainer_.scrollTop = this.currentDisplayTransform.panY;
if (changed)
this.dispatchChangeEvent();
},
onModelTrackControllerScroll_: function(e) {
if (this.dtAnimationController_.activeAnimation &&
this.dtAnimationController_.activeAnimation.affectsPanY)
this.dtAnimationController_.cancelActiveAnimation();
var panY = this.modelTrackContainer_.scrollTop;
this.currentDisplayTransform_.panY = panY;
},
get modelTrackContainer() {
return this.modelTrackContainer_;
},
set modelTrackContainer(m) {
if (this.modelTrackContainer_)
this.modelTrackContainer_.removeEventListener('scroll',
this.onModelTrackControllerScroll_);
this.modelTrackContainer_ = m;
this.modelTrackContainer_.addEventListener('scroll',
this.onModelTrackControllerScroll_);
},
get showFlowEvents() {
return this.showFlowEvents_;
},
set showFlowEvents(showFlowEvents) {
this.showFlowEvents_ = showFlowEvents;
this.dispatchChangeEvent();
},
get highlightVSync() {
return this.highlightVSync_;
},
set highlightVSync(highlightVSync) {
this.highlightVSync_ = highlightVSync;
this.dispatchChangeEvent();
},
get highDetails() {
return this.highDetails_;
},
set highDetails(highDetails) {
this.highDetails_ = highDetails;
this.dispatchChangeEvent();
},
get gridEnabled() {
return this.gridEnabled_;
},
set gridEnabled(enabled) {
if (this.gridEnabled_ === enabled)
return;
this.gridEnabled_ = enabled && true;
this.dispatchChangeEvent();
},
get gridTimebase() {
return this.gridTimebase_;
},
set gridTimebase(timebase) {
if (this.gridTimebase_ === timebase)
return;
this.gridTimebase_ = timebase;
this.dispatchChangeEvent();
},
get gridStep() {
return this.gridStep_;
},
get interestRange() {
return this.interestRange_;
},
get majorMarkWorldPositions() {
return this.majorMarkWorldPositions_;
},
get majorMarkUnit() {
switch (this.timeMode_) {
case TimelineViewport.TimeMode.TIME_IN_MS:
return tr.b.Unit.byName.timeInMsAutoFormat;
case TimelineViewport.TimeMode.REVISIONS:
return tr.b.Unit.byName.count;
default:
throw new Error(
'Cannot get Unit for unsupported time mode ' + this.timeMode_);
}
},
get timeMode() {
return this.timeMode_;
},
set timeMode(mode) {
this.timeMode_ = mode;
this.dispatchChangeEvent();
},
updateMajorMarkData: function(viewLWorld, viewRWorld) {
var pixelRatio = window.devicePixelRatio || 1;
var dt = this.currentDisplayTransform;
var idealMajorMarkDistancePix =
IDEAL_MAJOR_MARK_DISTANCE_PX * pixelRatio;
var idealMajorMarkDistanceWorld =
dt.xViewVectorToWorld(idealMajorMarkDistancePix);
var majorMarkDistanceWorld = tr.b.math.preferredNumberLargerThanMin(
idealMajorMarkDistanceWorld);
var firstMajorMark = Math.floor(
viewLWorld / majorMarkDistanceWorld) * majorMarkDistanceWorld;
this.majorMarkWorldPositions_ = [];
for (var curX = firstMajorMark;
curX < viewRWorld;
curX += majorMarkDistanceWorld) {
this.majorMarkWorldPositions_.push(
Math.floor(MAJOR_MARK_ROUNDING_FACTOR * curX) /
MAJOR_MARK_ROUNDING_FACTOR);
}
},
drawMajorMarkLines: function(ctx) {
// Apply subpixel translate to get crisp lines.
// http://www.mobtowers.com/html5-canvas-crisp-lines-every-time/
ctx.save();
ctx.translate((Math.round(ctx.lineWidth) % 2) / 2, 0);
ctx.beginPath();
for (var majorMark of this.majorMarkWorldPositions_) {
var x = this.currentDisplayTransform.xWorldToView(majorMark);
tr.ui.b.drawLine(ctx, x, 0, x, ctx.canvas.height);
}
ctx.strokeStyle = '#ddd';
ctx.stroke();
ctx.restore();
},
drawGridLines: function(ctx, viewLWorld, viewRWorld) {
if (!this.gridEnabled)
return;
var dt = this.currentDisplayTransform;
var x = this.gridTimebase;
// Apply subpixel translate to get crisp lines.
// http://www.mobtowers.com/html5-canvas-crisp-lines-every-time/
ctx.save();
ctx.translate((Math.round(ctx.lineWidth) % 2) / 2, 0);
ctx.beginPath();
while (x < viewRWorld) {
if (x >= viewLWorld) {
// Do conversion to viewspace here rather than on
// x to avoid precision issues.
var vx = Math.floor(dt.xWorldToView(x));
tr.ui.b.drawLine(ctx, vx, 0, vx, ctx.canvas.height);
}
x += this.gridStep;
}
ctx.strokeStyle = 'rgba(255, 0, 0, 0.25)';
ctx.stroke();
ctx.restore();
},
/**
* Helper for selection previous or next.
* @param {boolean} offset If positive, select one forward (next).
* Else, select previous.
*
* @return {boolean} true if current selection changed.
*/
getShiftedSelection: function(selection, offset) {
var newSelection = new tr.model.EventSet();
for (var event of selection) {
// If this is a flow event, then move to its slice based on the
// offset direction.
if (event instanceof tr.model.FlowEvent) {
if (offset > 0) {
newSelection.push(event.endSlice);
} else if (offset < 0) {
newSelection.push(event.startSlice);
} else {
/* Do nothing. Zero offsets don't do anything. */
}
continue;
}
var track = this.trackForEvent(event);
track.addEventNearToProvidedEventToSelection(
event, offset, newSelection);
}
if (newSelection.length === 0)
return undefined;
return newSelection;
},
rebuildEventToTrackMap: function() {
// TODO(charliea): Make the event to track map have a similar interface
// to the container to track map so that we can just clear() here.
this.eventToTrackMap_ = new tr.ui.tracks.EventToTrackMap();
this.modelTrackContainer_.addEventsToTrackMap(this.eventToTrackMap_);
},
rebuildContainerToTrackMap: function() {
this.containerToTrackMap.clear();
this.modelTrackContainer_.addContainersToTrackMap(
this.containerToTrackMap);
},
trackForEvent: function(event) {
return this.eventToTrackMap_[event.guid];
}
};
return {
TimelineViewport,
};
});
</script>