mirror of
https://github.com/react/react-native-devtools-frontend.git
synced 2026-10-10 21:54:47 +08:00
Change-Id: I15f5cc61fb956d2be00887eccad96af09c441a44 Reviewed-on: https://chromium-review.googlesource.com/1003833 Commit-Queue: Alexei Filippov <alph@chromium.org> Reviewed-by: Dmitry Gozman <dgozman@chromium.org> Cr-Original-Commit-Position: refs/heads/master@{#549821} Cr-Mirrored-From: https://chromium.googlesource.com/chromium/src Cr-Mirrored-Commit: 94942045f8a0d26994609c6c578170a6ebf5131b
524 lines
15 KiB
JavaScript
524 lines
15 KiB
JavaScript
// Copyright 2016 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.
|
|
|
|
/**
|
|
* @interface
|
|
*/
|
|
PerfUI.ChartViewportDelegate = function() {};
|
|
|
|
PerfUI.ChartViewportDelegate.prototype = {
|
|
/**
|
|
* @param {number} startTime
|
|
* @param {number} endTime
|
|
* @param {boolean} animate
|
|
*/
|
|
windowChanged(startTime, endTime, animate) {},
|
|
|
|
/**
|
|
* @param {number} startTime
|
|
* @param {number} endTime
|
|
*/
|
|
updateRangeSelection(startTime, endTime) {},
|
|
|
|
/**
|
|
* @param {number} width
|
|
* @param {number} height
|
|
*/
|
|
setSize(width, height) {},
|
|
|
|
update() {}
|
|
};
|
|
|
|
/**
|
|
* @unrestricted
|
|
*/
|
|
PerfUI.ChartViewport = class extends UI.VBox {
|
|
/**
|
|
* @param {!PerfUI.ChartViewportDelegate} delegate
|
|
*/
|
|
constructor(delegate) {
|
|
super();
|
|
this.registerRequiredCSS('perf_ui/chartViewport.css');
|
|
|
|
this._delegate = delegate;
|
|
|
|
this.viewportElement = this.contentElement.createChild('div', 'fill');
|
|
this.viewportElement.addEventListener('mousemove', this._updateCursorPosition.bind(this), false);
|
|
this.viewportElement.addEventListener('mouseout', this._onMouseOut.bind(this), false);
|
|
this.viewportElement.addEventListener('mousewheel', this._onMouseWheel.bind(this), false);
|
|
this.viewportElement.addEventListener('keydown', this._onChartKeyDown.bind(this), false);
|
|
this.viewportElement.addEventListener('keyup', this._onChartKeyUp.bind(this), false);
|
|
|
|
UI.installDragHandle(
|
|
this.viewportElement, this._startDragging.bind(this), this._dragging.bind(this), this._endDragging.bind(this),
|
|
'-webkit-grabbing', null);
|
|
UI.installDragHandle(
|
|
this.viewportElement, this._startRangeSelection.bind(this), this._rangeSelectionDragging.bind(this),
|
|
this._endRangeSelection.bind(this), 'text', null);
|
|
|
|
this._alwaysShowVerticalScroll = false;
|
|
this._rangeSelectionEnabled = true;
|
|
this._vScrollElement = this.contentElement.createChild('div', 'chart-viewport-v-scroll');
|
|
this._vScrollContent = this._vScrollElement.createChild('div');
|
|
this._vScrollElement.addEventListener('scroll', this._onScroll.bind(this), false);
|
|
|
|
this._selectionOverlay = this.contentElement.createChild('div', 'chart-viewport-selection-overlay hidden');
|
|
this._selectedTimeSpanLabel = this._selectionOverlay.createChild('div', 'time-span');
|
|
|
|
this._cursorElement = this.contentElement.createChild('div', 'chart-cursor-element hidden');
|
|
|
|
this.reset();
|
|
}
|
|
|
|
alwaysShowVerticalScroll() {
|
|
this._alwaysShowVerticalScroll = true;
|
|
this._vScrollElement.classList.add('always-show-scrollbar');
|
|
}
|
|
|
|
disableRangeSelection() {
|
|
this._rangeSelectionEnabled = false;
|
|
this._rangeSelectionStart = null;
|
|
this._rangeSelectionEnd = null;
|
|
this._updateRangeSelectionOverlay();
|
|
}
|
|
|
|
/**
|
|
* @return {boolean}
|
|
*/
|
|
isDragging() {
|
|
return this._isDragging;
|
|
}
|
|
|
|
/**
|
|
* @override
|
|
* @return {!Array<!Element>}
|
|
*/
|
|
elementsToRestoreScrollPositionsFor() {
|
|
return [this._vScrollElement];
|
|
}
|
|
|
|
_updateScrollBar() {
|
|
const showScroll = this._alwaysShowVerticalScroll || this._totalHeight > this._offsetHeight;
|
|
if (this._vScrollElement.classList.contains('hidden') !== showScroll)
|
|
return;
|
|
this._vScrollElement.classList.toggle('hidden', !showScroll);
|
|
this._updateContentElementSize();
|
|
}
|
|
|
|
/**
|
|
* @override
|
|
*/
|
|
onResize() {
|
|
this._updateScrollBar();
|
|
this._updateContentElementSize();
|
|
this.scheduleUpdate();
|
|
}
|
|
|
|
reset() {
|
|
this._vScrollElement.scrollTop = 0;
|
|
this._scrollTop = 0;
|
|
this._rangeSelectionStart = null;
|
|
this._rangeSelectionEnd = null;
|
|
this._isDragging = false;
|
|
this._dragStartPointX = 0;
|
|
this._dragStartPointY = 0;
|
|
this._dragStartScrollTop = 0;
|
|
this._visibleLeftTime = 0;
|
|
this._visibleRightTime = 0;
|
|
this._offsetWidth = 0;
|
|
this._offsetHeight = 0;
|
|
this._totalHeight = 0;
|
|
this._targetLeftTime = 0;
|
|
this._targetRightTime = 0;
|
|
this._updateContentElementSize();
|
|
}
|
|
|
|
_updateContentElementSize() {
|
|
let offsetWidth = this._vScrollElement.offsetLeft;
|
|
if (!offsetWidth)
|
|
offsetWidth = this.contentElement.offsetWidth;
|
|
this._offsetWidth = offsetWidth;
|
|
this._offsetHeight = this.contentElement.offsetHeight;
|
|
this._delegate.setSize(this._offsetWidth, this._offsetHeight);
|
|
}
|
|
|
|
/**
|
|
* @param {number} totalHeight
|
|
*/
|
|
setContentHeight(totalHeight) {
|
|
this._totalHeight = totalHeight;
|
|
this._vScrollContent.style.height = totalHeight + 'px';
|
|
this._updateScrollBar();
|
|
if (this._scrollTop + this._offsetHeight <= totalHeight)
|
|
return;
|
|
this._scrollTop = Math.max(0, totalHeight - this._offsetHeight);
|
|
this._vScrollElement.scrollTop = this._scrollTop;
|
|
}
|
|
|
|
/**
|
|
* @param {number} offset
|
|
* @param {number=} height
|
|
*/
|
|
setScrollOffset(offset, height) {
|
|
height = height || 0;
|
|
if (this._vScrollElement.scrollTop > offset)
|
|
this._vScrollElement.scrollTop = offset;
|
|
else if (this._vScrollElement.scrollTop < offset - this._offsetHeight + height)
|
|
this._vScrollElement.scrollTop = offset - this._offsetHeight + height;
|
|
}
|
|
|
|
/**
|
|
* @return {number}
|
|
*/
|
|
scrollOffset() {
|
|
return this._vScrollElement.scrollTop;
|
|
}
|
|
|
|
/**
|
|
* @param {number} zeroTime
|
|
* @param {number} totalTime
|
|
*/
|
|
setBoundaries(zeroTime, totalTime) {
|
|
this._minimumBoundary = zeroTime;
|
|
this._totalTime = totalTime;
|
|
}
|
|
|
|
/**
|
|
* @param {!Event} e
|
|
*/
|
|
_onMouseWheel(e) {
|
|
const doZoomInstead = e.shiftKey ^ (Common.moduleSetting('flamechartMouseWheelAction').get() === 'zoom');
|
|
const panVertically = !doZoomInstead && (e.wheelDeltaY || Math.abs(e.wheelDeltaX) === 120);
|
|
const panHorizontally = doZoomInstead && Math.abs(e.wheelDeltaX) > Math.abs(e.wheelDeltaY);
|
|
if (panVertically) {
|
|
this._vScrollElement.scrollTop -= (e.wheelDeltaY || e.wheelDeltaX) / 120 * this._offsetHeight / 8;
|
|
} else if (panHorizontally) {
|
|
this._handlePanGesture(-e.wheelDeltaX, /* animate */ true);
|
|
} else { // Zoom.
|
|
const mouseWheelZoomSpeed = 1 / 120;
|
|
this._handleZoomGesture(Math.pow(1.2, -(e.wheelDeltaY || e.wheelDeltaX) * mouseWheelZoomSpeed) - 1);
|
|
}
|
|
|
|
// Block swipe gesture.
|
|
e.consume(true);
|
|
}
|
|
|
|
/**
|
|
* @param {!MouseEvent} event
|
|
* @return {boolean}
|
|
*/
|
|
_startDragging(event) {
|
|
if (event.shiftKey)
|
|
return false;
|
|
this._isDragging = true;
|
|
this._dragStartPointX = event.pageX;
|
|
this._dragStartPointY = event.pageY;
|
|
this._dragStartScrollTop = this._vScrollElement.scrollTop;
|
|
this.viewportElement.style.cursor = '';
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* @param {!MouseEvent} event
|
|
*/
|
|
_dragging(event) {
|
|
const pixelShift = this._dragStartPointX - event.pageX;
|
|
this._dragStartPointX = event.pageX;
|
|
this._handlePanGesture(pixelShift);
|
|
const pixelScroll = this._dragStartPointY - event.pageY;
|
|
this._vScrollElement.scrollTop = this._dragStartScrollTop + pixelScroll;
|
|
}
|
|
|
|
_endDragging() {
|
|
this._isDragging = false;
|
|
}
|
|
|
|
/**
|
|
* @param {!MouseEvent} event
|
|
* @return {boolean}
|
|
*/
|
|
_startRangeSelection(event) {
|
|
if (!event.shiftKey || !this._rangeSelectionEnabled)
|
|
return false;
|
|
this._isDragging = true;
|
|
this._selectionOffsetShiftX = event.offsetX - event.pageX;
|
|
this._selectionOffsetShiftY = event.offsetY - event.pageY;
|
|
this._selectionStartX = event.offsetX;
|
|
const style = this._selectionOverlay.style;
|
|
style.left = this._selectionStartX + 'px';
|
|
style.width = '1px';
|
|
this._selectedTimeSpanLabel.textContent = '';
|
|
this._selectionOverlay.classList.remove('hidden');
|
|
return true;
|
|
}
|
|
|
|
_endRangeSelection() {
|
|
this._isDragging = false;
|
|
this._selectionStartX = null;
|
|
}
|
|
|
|
hideRangeSelection() {
|
|
this._selectionOverlay.classList.add('hidden');
|
|
this._rangeSelectionStart = null;
|
|
this._rangeSelectionEnd = null;
|
|
}
|
|
|
|
/**
|
|
* @param {number} startTime
|
|
* @param {number} endTime
|
|
*/
|
|
setRangeSelection(startTime, endTime) {
|
|
if (!this._rangeSelectionEnabled)
|
|
return;
|
|
this._rangeSelectionStart = Math.min(startTime, endTime);
|
|
this._rangeSelectionEnd = Math.max(startTime, endTime);
|
|
this._updateRangeSelectionOverlay();
|
|
this._delegate.updateRangeSelection(this._rangeSelectionStart, this._rangeSelectionEnd);
|
|
}
|
|
|
|
/**
|
|
* @param {!Event} event
|
|
*/
|
|
onClick(event) {
|
|
const time = this.pixelToTime(event.offsetX);
|
|
if (this._rangeSelectionStart !== null && time >= this._rangeSelectionStart && time <= this._rangeSelectionEnd)
|
|
return;
|
|
this.hideRangeSelection();
|
|
}
|
|
|
|
/**
|
|
* @param {!MouseEvent} event
|
|
*/
|
|
_rangeSelectionDragging(event) {
|
|
const x = Number.constrain(event.pageX + this._selectionOffsetShiftX, 0, this._offsetWidth);
|
|
const start = this.pixelToTime(this._selectionStartX);
|
|
const end = this.pixelToTime(x);
|
|
this.setRangeSelection(start, end);
|
|
}
|
|
|
|
_updateRangeSelectionOverlay() {
|
|
const /** @const */ margin = 100;
|
|
const left = Number.constrain(this.timeToPosition(this._rangeSelectionStart), -margin, this._offsetWidth + margin);
|
|
const right = Number.constrain(this.timeToPosition(this._rangeSelectionEnd), -margin, this._offsetWidth + margin);
|
|
const style = this._selectionOverlay.style;
|
|
style.left = left + 'px';
|
|
style.width = (right - left) + 'px';
|
|
const timeSpan = this._rangeSelectionEnd - this._rangeSelectionStart;
|
|
this._selectedTimeSpanLabel.textContent = Number.preciseMillisToString(timeSpan, 2);
|
|
}
|
|
|
|
_onScroll() {
|
|
this._scrollTop = this._vScrollElement.scrollTop;
|
|
this.scheduleUpdate();
|
|
}
|
|
|
|
_onMouseOut() {
|
|
this._lastMouseOffsetX = -1;
|
|
this._showCursor(false);
|
|
}
|
|
|
|
/**
|
|
* @param {!Event} e
|
|
*/
|
|
_updateCursorPosition(e) {
|
|
this._showCursor(e.shiftKey);
|
|
this._cursorElement.style.left = e.offsetX + 'px';
|
|
this._lastMouseOffsetX = e.offsetX;
|
|
}
|
|
|
|
/**
|
|
* @param {number} x
|
|
* @return {number}
|
|
*/
|
|
pixelToTime(x) {
|
|
return this.pixelToTimeOffset(x) + this._visibleLeftTime;
|
|
}
|
|
|
|
/**
|
|
* @param {number} x
|
|
* @return {number}
|
|
*/
|
|
pixelToTimeOffset(x) {
|
|
return x * (this._visibleRightTime - this._visibleLeftTime) / this._offsetWidth;
|
|
}
|
|
|
|
/**
|
|
* @param {number} time
|
|
* @return {number}
|
|
*/
|
|
timeToPosition(time) {
|
|
return Math.floor(
|
|
(time - this._visibleLeftTime) / (this._visibleRightTime - this._visibleLeftTime) * this._offsetWidth);
|
|
}
|
|
|
|
/**
|
|
* @return {number}
|
|
*/
|
|
timeToPixel() {
|
|
return this._offsetWidth / (this._visibleRightTime - this._visibleLeftTime);
|
|
}
|
|
|
|
/**
|
|
* @param {boolean} visible
|
|
*/
|
|
_showCursor(visible) {
|
|
this._cursorElement.classList.toggle('hidden', !visible || this._isDragging);
|
|
}
|
|
|
|
/**
|
|
* @param {!Event} e
|
|
*/
|
|
_onChartKeyDown(e) {
|
|
this._showCursor(e.shiftKey);
|
|
this._handleZoomPanKeys(e);
|
|
}
|
|
|
|
/**
|
|
* @param {!Event} e
|
|
*/
|
|
_onChartKeyUp(e) {
|
|
this._showCursor(e.shiftKey);
|
|
}
|
|
|
|
/**
|
|
* @param {!Event} e
|
|
*/
|
|
_handleZoomPanKeys(e) {
|
|
if (!UI.KeyboardShortcut.hasNoModifiers(e))
|
|
return;
|
|
const zoomFactor = e.shiftKey ? 0.8 : 0.3;
|
|
const panOffset = e.shiftKey ? 320 : 160;
|
|
switch (e.code) {
|
|
case 'KeyA':
|
|
this._handlePanGesture(-panOffset, /* animate */ true);
|
|
break;
|
|
case 'KeyD':
|
|
this._handlePanGesture(panOffset, /* animate */ true);
|
|
break;
|
|
case 'KeyW':
|
|
this._handleZoomGesture(-zoomFactor);
|
|
break;
|
|
case 'KeyS':
|
|
this._handleZoomGesture(zoomFactor);
|
|
break;
|
|
default:
|
|
return;
|
|
}
|
|
e.consume(true);
|
|
}
|
|
|
|
/**
|
|
* @param {number} zoom
|
|
*/
|
|
_handleZoomGesture(zoom) {
|
|
const bounds = {left: this._targetLeftTime, right: this._targetRightTime};
|
|
const cursorTime = this.pixelToTime(this._lastMouseOffsetX);
|
|
bounds.left += (bounds.left - cursorTime) * zoom;
|
|
bounds.right += (bounds.right - cursorTime) * zoom;
|
|
this._requestWindowTimes(bounds, /* animate */ true);
|
|
}
|
|
|
|
/**
|
|
* @param {number} offset
|
|
* @param {boolean=} animate
|
|
*/
|
|
_handlePanGesture(offset, animate) {
|
|
const bounds = {left: this._targetLeftTime, right: this._targetRightTime};
|
|
const timeOffset = Number.constrain(
|
|
this.pixelToTimeOffset(offset), this._minimumBoundary - bounds.left,
|
|
this._totalTime + this._minimumBoundary - bounds.right);
|
|
bounds.left += timeOffset;
|
|
bounds.right += timeOffset;
|
|
this._requestWindowTimes(bounds, !!animate);
|
|
}
|
|
|
|
/**
|
|
* @param {!{left: number, right: number}} bounds
|
|
* @param {boolean} animate
|
|
*/
|
|
_requestWindowTimes(bounds, animate) {
|
|
const maxBound = this._minimumBoundary + this._totalTime;
|
|
if (bounds.left < this._minimumBoundary) {
|
|
bounds.right = Math.min(bounds.right + this._minimumBoundary - bounds.left, maxBound);
|
|
bounds.left = this._minimumBoundary;
|
|
} else if (bounds.right > maxBound) {
|
|
bounds.left = Math.max(bounds.left - bounds.right + maxBound, this._minimumBoundary);
|
|
bounds.right = maxBound;
|
|
}
|
|
if (bounds.right - bounds.left < PerfUI.FlameChart.MinimalTimeWindowMs)
|
|
return;
|
|
this._delegate.windowChanged(bounds.left, bounds.right, animate);
|
|
}
|
|
|
|
scheduleUpdate() {
|
|
if (this._updateTimerId || this._cancelWindowTimesAnimation)
|
|
return;
|
|
this._updateTimerId = this.element.window().requestAnimationFrame(() => {
|
|
this._updateTimerId = 0;
|
|
this._update();
|
|
});
|
|
}
|
|
|
|
_update() {
|
|
this._updateRangeSelectionOverlay();
|
|
this._delegate.update();
|
|
}
|
|
|
|
/**
|
|
* @param {number} startTime
|
|
* @param {number} endTime
|
|
* @param {boolean=} animate
|
|
*/
|
|
setWindowTimes(startTime, endTime, animate) {
|
|
if (startTime === this._targetLeftTime && endTime === this._targetRightTime)
|
|
return;
|
|
if (!animate || this._visibleLeftTime === 0 || this._visibleRightTime === Infinity ||
|
|
(startTime === 0 && endTime === Infinity) || (startTime === Infinity && endTime === Infinity)) {
|
|
// Skip animation, move instantly.
|
|
this._targetLeftTime = startTime;
|
|
this._targetRightTime = endTime;
|
|
this._visibleLeftTime = startTime;
|
|
this._visibleRightTime = endTime;
|
|
this.scheduleUpdate();
|
|
return;
|
|
}
|
|
if (this._cancelWindowTimesAnimation) {
|
|
this._cancelWindowTimesAnimation();
|
|
this._visibleLeftTime = this._targetLeftTime;
|
|
this._visibleRightTime = this._targetRightTime;
|
|
}
|
|
this._targetLeftTime = startTime;
|
|
this._targetRightTime = endTime;
|
|
this._cancelWindowTimesAnimation = UI.animateFunction(
|
|
this.element.window(), animateWindowTimes.bind(this),
|
|
[{from: this._visibleLeftTime, to: startTime}, {from: this._visibleRightTime, to: endTime}], 100,
|
|
() => this._cancelWindowTimesAnimation = null);
|
|
|
|
/**
|
|
* @param {number} startTime
|
|
* @param {number} endTime
|
|
* @this {PerfUI.ChartViewport}
|
|
*/
|
|
function animateWindowTimes(startTime, endTime) {
|
|
this._visibleLeftTime = startTime;
|
|
this._visibleRightTime = endTime;
|
|
this._update();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @return {number}
|
|
*/
|
|
windowLeftTime() {
|
|
return this._visibleLeftTime;
|
|
}
|
|
|
|
/**
|
|
* @return {number}
|
|
*/
|
|
windowRightTime() {
|
|
return this._visibleRightTime;
|
|
}
|
|
};
|