All Downloads are FREE. Search and download functionalities are using the official Maven repository.

META-INF.dirigible.dev-tools.perf_ui.ChartViewport.js Maven / Gradle / Ivy

There is a newer version: 10.6.27
Show newest version
// 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.

import * as Common from '../common/common.js';
import * as Platform from '../platform/platform.js';
import * as UI from '../ui/ui.js';

import {MinimalTimeWindowMs} from './FlameChart.js';

/**
 * @interface
 */
export class ChartViewportDelegate {
  /**
   * @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
 */
export class ChartViewport extends UI.Widget.VBox {
  /**
   * @param {!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.UIUtils.installDragHandle(
        this.viewportElement, this._startDragging.bind(this), this._dragging.bind(this), this._endDragging.bind(this),
        '-webkit-grabbing', null);
    UI.UIUtils.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}
   */
  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();
    this._updateContentElementSize();
    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;
  }

  /**
   * @return {number}
   */
  chartHeight() {
    return this._offsetHeight;
  }

  /**
   * @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.Settings.Settings.instance().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 = Platform.NumberUtilities.clamp(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 = Platform.NumberUtilities.clamp(
        this.timeToPosition(this._rangeSelectionStart), -margin, this._offsetWidth + margin);
    const right = Platform.NumberUtilities.clamp(
        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.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 = Platform.NumberUtilities.clamp(
        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 < 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.UIUtils.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 {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;
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy