text: // 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.
  
  // @ts-nocheck
  // TODO(crbug.com/1011811): Enable TypeScript compiler checks
  
  import * as Common from '../common/common.js';
  import * as PerfUI from '../perf_ui/perf_ui.js';
  import * as SDK from '../sdk/sdk.js';  // eslint-disable-line no-unused-vars
  import * as UI from '../ui/ui.js';
  
  import {NetworkNode} from './NetworkDataGridNode.js';              // eslint-disable-line no-unused-vars
  import {NetworkTimeCalculator} from './NetworkTimeCalculator.js';  // eslint-disable-line no-unused-vars
  import {RequestTimeRangeNames, RequestTimingView} from './RequestTimingView.js';
  
  export class NetworkWaterfallColumn extends UI.Widget.VBox {
    /**
     * @param {!NetworkTimeCalculator} calculator
     */
    constructor(calculator) {
      // TODO(allada) Make this a shadowDOM when the NetworkWaterfallColumn gets moved into NetworkLogViewColumns.
      super(false);
      this.registerRequiredCSS('network/networkWaterfallColumn.css');
  
      this._canvas = this.contentElement.createChild('canvas');
      this._canvas.tabIndex = -1;
      this.setDefaultFocusedElement(this._canvas);
      this._canvasPosition = this._canvas.getBoundingClientRect();
  
      /** @const */
      this._leftPadding = 5;
      /** @const */
      this._fontSize = 10;
  
      this._rightPadding = 0;
      this._scrollTop = 0;
  
      this._headerHeight = 0;
      this._calculator = calculator;
  
      // this._rawRowHeight captures model height (41 or 21px),
      // this._rowHeight is computed height of the row in CSS pixels, can be 20.8 for zoomed-in content.
      this._rawRowHeight = 0;
      this._rowHeight = 0;
  
      this._offsetWidth = 0;
      this._offsetHeight = 0;
      this._startTime = this._calculator.minimumBoundary();
      this._endTime = this._calculator.maximumBoundary();
  
      this._popoverHelper = new UI.PopoverHelper.PopoverHelper(this.element, this._getPopoverRequest.bind(this));
      this._popoverHelper.setHasPadding(true);
      this._popoverHelper.setTimeout(300, 300);
  
      /** @type {!Array<!NetworkNode>} */
      this._nodes = [];
  
      /** @type {?NetworkNode} */
      this._hoveredNode = null;
  
      /** @type {!Map<string, !Array<number>>} */
      this._eventDividers = new Map();
  
      /** @type {(number|undefined)} */
      this._updateRequestID;
  
      this.element.addEventListener('mousemove', this._onMouseMove.bind(this), true);
      this.element.addEventListener('mouseleave', event => this._setHoveredNode(null, false), true);
      this.element.addEventListener('click', this._onClick.bind(this), true);
  
      this._styleForTimeRangeName = NetworkWaterfallColumn._buildRequestTimeRangeStyle();
  
      const resourceStyleTuple = NetworkWaterfallColumn._buildResourceTypeStyle();
      /** @type {!Map<!Common.ResourceType.ResourceType, !_LayerStyle>} */
      this._styleForWaitingResourceType = resourceStyleTuple[0];
      /** @type {!Map<!Common.ResourceType.ResourceType, !_LayerStyle>} */
      this._styleForDownloadingResourceType = resourceStyleTuple[1];
  
      const baseLineColor = self.UI.themeSupport.patchColorText('#a5a5a5', UI.UIUtils.ThemeSupport.ColorUsage.Foreground);
      /** @type {!_LayerStyle} */
      this._wiskerStyle = {borderColor: baseLineColor, lineWidth: 1};
      /** @type {!_LayerStyle} */
      this._hoverDetailsStyle = {fillStyle: baseLineColor, lineWidth: 1, borderColor: baseLineColor};
  
      /** @type {!Map<!_LayerStyle, !Path2D>} */
      this._pathForStyle = new Map();
      /** @type {!Array<!_TextLayer>} */
      this._textLayers = [];
    }
  
    /**
     * @return {!Map<!RequestTimeRangeNames, !_LayerStyle>}
     */
    static _buildRequestTimeRangeStyle() {
      const types = RequestTimeRangeNames;
      const styleMap = new Map();
      styleMap.set(types.Connecting, {fillStyle: '#FF9800'});
      styleMap.set(types.SSL, {fillStyle: '#9C27B0'});
      styleMap.set(types.DNS, {fillStyle: '#009688'});
      styleMap.set(types.Proxy, {fillStyle: '#A1887F'});
      styleMap.set(types.Blocking, {fillStyle: '#AAAAAA'});
      styleMap.set(types.Push, {fillStyle: '#8CDBff'});
      styleMap.set(types.Queueing, {fillStyle: 'white', lineWidth: 2, borderColor: 'lightgrey'});
      // This ensures we always show at least 2 px for a request.
      styleMap.set(types.Receiving, {fillStyle: '#03A9F4', lineWidth: 2, borderColor: '#03A9F4'});
      styleMap.set(types.Waiting, {fillStyle: '#00C853'});
      styleMap.set(types.ReceivingPush, {fillStyle: '#03A9F4'});
      styleMap.set(types.ServiceWorker, {fillStyle: 'orange'});
      styleMap.set(types.ServiceWorkerPreparation, {fillStyle: 'orange'});
      styleMap.set(types.ServiceWorkerRespondWith, {fillStyle: '#A8A3FF'});
      return styleMap;
    }
  
    /**
     * @return {!Array<!Map<!Common.ResourceType.ResourceType, !_LayerStyle>>}
     */
    static _buildResourceTypeStyle() {
      const baseResourceTypeColors = new Map([
        ['document', 'hsl(215, 100%, 80%)'],
        ['font', 'hsl(8, 100%, 80%)'],
        ['media', 'hsl(90, 50%, 80%)'],
        ['image', 'hsl(90, 50%, 80%)'],
        ['script', 'hsl(31, 100%, 80%)'],
        ['stylesheet', 'hsl(272, 64%, 80%)'],
        ['texttrack', 'hsl(8, 100%, 80%)'],
        ['websocket', 'hsl(0, 0%, 95%)'],
        ['xhr', 'hsl(53, 100%, 80%)'],
        ['fetch', 'hsl(53, 100%, 80%)'],
        ['other', 'hsl(0, 0%, 95%)'],
      ]);
      const waitingStyleMap = new Map();
      const downloadingStyleMap = new Map();
  
      for (const resourceType of Object.values(Common.ResourceType.resourceTypes)) {
        let color = baseResourceTypeColors.get(resourceType.name());
        if (!color) {
          color = baseResourceTypeColors.get('other');
        }
        const borderColor = toBorderColor(color);
  
        waitingStyleMap.set(resourceType, {fillStyle: toWaitingColor(color), lineWidth: 1, borderColor: borderColor});
        downloadingStyleMap.set(resourceType, {fillStyle: color, lineWidth: 1, borderColor: borderColor});
      }
      return [waitingStyleMap, downloadingStyleMap];
  
      /**
       * @param {string} color
       */
      function toBorderColor(color) {
        const parsedColor = Common.Color.Color.parse(color);
        const hsla = parsedColor.hsla();
        hsla[1] /= 2;
        hsla[2] -= Math.min(hsla[2], 0.2);
        return parsedColor.asString(null);
      }
  
      /**
       * @param {string} color
       */
      function toWaitingColor(color) {
        const parsedColor = Common.Color.Color.parse(color);
        const hsla = parsedColor.hsla();
        hsla[2] *= 1.1;
        return parsedColor.asString(null);
      }
    }
  
    _resetPaths() {
      this._pathForStyle.clear();
      this._pathForStyle.set(this._wiskerStyle, new Path2D());
      this._styleForTimeRangeName.forEach(style => this._pathForStyle.set(style, new Path2D()));
      this._styleForWaitingResourceType.forEach(style => this._pathForStyle.set(style, new Path2D()));
      this._styleForDownloadingResourceType.forEach(style => this._pathForStyle.set(style, new Path2D()));
      this._pathForStyle.set(this._hoverDetailsStyle, new Path2D());
    }
  
    /**
     * @override
     */
    willHide() {
      this._popoverHelper.hidePopover();
    }
  
    /**
     * @override
     */
    wasShown() {
      this.update();
    }
  
    /**
     * @param {!Event} event
     */
    _onMouseMove(event) {
      this._setHoveredNode(this.getNodeFromPoint(event.offsetX, event.offsetY), event.shiftKey);
    }
  
    /**
     * @param {!Event} event
     */
    _onClick(event) {
      const handled = this._setSelectedNode(this.getNodeFromPoint(event.offsetX, event.offsetY));
      if (handled) {
        event.consume(true);
      }
    }
  
    /**
     * @param {!Event} event
     * @return {?UI.PopoverRequest}
     */
    _getPopoverRequest(event) {
      if (!this._hoveredNode) {
        return null;
      }
      const request = this._hoveredNode.request();
      if (!request) {
        return null;
      }
      const useTimingBars = !Common.Settings.Settings.instance().moduleSetting('networkColorCodeResourceTypes').get() &&
          !this._calculator.startAtZero;
      let range;
      let start;
      let end;
      if (useTimingBars) {
        range = RequestTimingView.calculateRequestTimeRanges(request, 0)
                    .find(data => data.name === RequestTimeRangeNames.Total);
        start = this._timeToPosition(range.start);
        end = this._timeToPosition(range.end);
      } else {
        range = this._getSimplifiedBarRange(request, 0);
        start = range.start;
        end = range.end;
      }
  
      if (end - start < 50) {
        const halfWidth = (end - start) / 2;
        start = start + halfWidth - 25;
        end = end - halfWidth + 25;
      }
  
      if (event.clientX < this._canvasPosition.left + start || event.clientX > this._canvasPosition.left + end) {
        return null;
      }
  
      const rowIndex = this._nodes.findIndex(node => node.hovered());
      const barHeight = this._getBarHeight(range.name);
      const y = this._headerHeight + (this._rowHeight * rowIndex - this._scrollTop) + ((this._rowHeight - barHeight) / 2);
  
      if (event.clientY < this._canvasPosition.top + y || event.clientY > this._canvasPosition.top + y + barHeight) {
        return null;
      }
  
      const anchorBox = this.element.boxInWindow();
      anchorBox.x += start;
      anchorBox.y += y;
      anchorBox.width = end - start;
      anchorBox.height = barHeight;
  
      return {
        box: anchorBox,
        show: popover => {
          const content = RequestTimingView.createTimingTable(
              /** @type {!SDK.NetworkRequest.NetworkRequest} */ (request), this._calculator);
          popover.contentElement.appendChild(content);
          return Promise.resolve(true);
        }
      };
    }
  
    /**
     * @param {?NetworkNode} node
     * @param {boolean} highlightInitiatorChain
     */
    _setHoveredNode(node, highlightInitiatorChain) {
      if (this._hoveredNode) {
        this._hoveredNode.setHovered(false, false);
      }
      this._hoveredNode = node;
      if (this._hoveredNode) {
        this._hoveredNode.setHovered(true, highlightInitiatorChain);
      }
    }
  
    /**
     * @param {?NetworkNode} node
     * @returns {boolean}
     */
    _setSelectedNode(node) {
      if (node && node.dataGrid) {
        node.select();
        node.dataGrid.element.focus();
        return true;
      }
      return false;
    }
  
    /**
     * @param {number} height
     */
    setRowHeight(height) {
      this._rawRowHeight = height;
      this._updateRowHeight();
    }
  
    _updateRowHeight() {
      this._rowHeight = Math.round(this._rawRowHeight * window.devicePixelRatio) / window.devicePixelRatio;
    }
  
    /**
     * @param {number} height
     */
    setHeaderHeight(height) {
      this._headerHeight = height;
    }
  
    /**
     * @param {number} padding
     */
    setRightPadding(padding) {
      this._rightPadding = padding;
      this._calculateCanvasSize();
    }
  
    /**
     * @param {!NetworkTimeCalculator} calculator
     */
    setCalculator(calculator) {
      this._calculator = calculator;
    }
  
    /**
     * @param {number} x
     * @param {number} y
     * @return {?NetworkNode}
     */
    getNodeFromPoint(x, y) {
      if (y <= this._headerHeight) {
        return null;
      }
      return this._nodes[Math.floor((this._scrollTop + y - this._headerHeight) / this._rowHeight)];
    }
  
    scheduleDraw() {
      if (this._updateRequestID) {
        return;
      }
      this._updateRequestID = this.element.window().requestAnimationFrame(() => this.update());
    }
  
    /**
     * @param {number=} scrollTop
     * @param {!Map<string, !Array<number>>=} eventDividers
     * @param {!Array<!NetworkNode>=} nodes
     */
    update(scrollTop, eventDividers, nodes) {
      if (scrollTop !== undefined && this._scrollTop !== scrollTop) {
        this._popoverHelper.hidePopover();
        this._scrollTop = scrollTop;
      }
      if (nodes) {
        this._nodes = nodes;
        this._calculateCanvasSize();
      }
      if (eventDividers !== undefined) {
        this._eventDividers = eventDividers;
      }
      if (this._updateRequestID) {
        this.element.window().cancelAnimationFrame(this._updateRequestID);
        delete this._updateRequestID;
      }
  
      this._startTime = this._calculator.minimumBoundary();
      this._endTime = this._calculator.maximumBoundary();
      this._resetCanvas();
      this._resetPaths();
      this._textLayers = [];
      this._draw();
    }
  
    _resetCanvas() {
      const ratio = window.devicePixelRatio;
      this._canvas.width = this._offsetWidth * ratio;
      this._canvas.height = this._offsetHeight * ratio;
      this._canvas.style.width = this._offsetWidth + 'px';
      this._canvas.style.height = this._offsetHeight + 'px';
    }
  
    /**
     * @override
     */
    onResize() {
      super.onResize();
      this._updateRowHeight();
      this._calculateCanvasSize();
      this.scheduleDraw();
    }
  
    _calculateCanvasSize() {
      this._offsetWidth = this.contentElement.offsetWidth - this._rightPadding;
      this._offsetHeight = this.contentElement.offsetHeight;
      this._calculator.setDisplayWidth(this._offsetWidth);
      this._canvasPosition = this._canvas.getBoundingClientRect();
    }
  
    /**
     * @param {number} time
     * @return {number}
     */
    _timeToPosition(time) {
      const availableWidth = this._offsetWidth - this._leftPadding;
      const timeToPixel = availableWidth / (this._endTime - this._startTime);
      return Math.floor(this._leftPadding + (time - this._startTime) * timeToPixel);
    }
  
    _didDrawForTest() {
    }
  
    _draw() {
      const useTimingBars = !Common.Settings.Settings.instance().moduleSetting('networkColorCodeResourceTypes').get() &&
          !this._calculator.startAtZero;
      const nodes = this._nodes;
      const context = this._canvas.getContext('2d');
      context.save();
      context.scale(window.devicePixelRatio, window.devicePixelRatio);
      context.translate(0, this._headerHeight);
      context.rect(0, 0, this._offsetWidth, this._offsetHeight);
      context.clip();
      const firstRequestIndex = Math.floor(this._scrollTop / this._rowHeight);
      const lastRequestIndex =
          Math.min(nodes.length, firstRequestIndex + Math.ceil(this._offsetHeight / this._rowHeight));
      for (let i = firstRequestIndex; i < lastRequestIndex; i++) {
        const rowOffset = this._rowHeight * i;
        const node = nodes[i];
        this._decorateRow(context, node, rowOffset - this._scrollTop);
        let drawNodes = [];
        if (node.hasChildren() && !node.expanded) {
          drawNodes = /** @type {!Array<!NetworkNode>} */ (node.flatChildren());
        }
        drawNodes.push(node);
        for (const drawNode of drawNodes) {
          if (useTimingBars) {
            this._buildTimingBarLayers(drawNode, rowOffset - this._scrollTop);
          } else {
            this._buildSimplifiedBarLayers(context, drawNode, rowOffset - this._scrollTop);
          }
        }
      }
      this._drawLayers(context);
  
      context.save();
      context.fillStyle = self.UI.themeSupport.patchColorText('#888', UI.UIUtils.ThemeSupport.ColorUsage.Foreground);
      for (const textData of this._textLayers) {
        context.fillText(textData.text, textData.x, textData.y);
      }
      context.restore();
  
      this._drawEventDividers(context);
      context.restore();
  
      const freeZoneAtLeft = 75;
      const freeZoneAtRight = 18;
      const dividersData = PerfUI.TimelineGrid.TimelineGrid.calculateGridOffsets(this._calculator);
      PerfUI.TimelineGrid.TimelineGrid.drawCanvasGrid(context, dividersData);
      PerfUI.TimelineGrid.TimelineGrid.drawCanvasHeaders(
          context, dividersData, time => this._calculator.formatValue(time, dividersData.precision), this._fontSize,
          this._headerHeight, freeZoneAtLeft);
      context.clearRect(this._offsetWidth - freeZoneAtRight, 0, freeZoneAtRight, this._headerHeight);
      this._didDrawForTest();
    }
  
    /**
     * @param {!CanvasRenderingContext2D} context
     */
    _drawLayers(context) {
      for (const entry of this._pathForStyle) {
        const style = /** @type {!_LayerStyle} */ (entry[0]);
        const path = /** @type {!Path2D} */ (entry[1]);
        context.save();
        context.beginPath();
        if (style.lineWidth) {
          context.lineWidth = style.lineWidth;
          context.strokeStyle = style.borderColor;
          context.stroke(path);
        }
        if (style.fillStyle) {
          context.fillStyle = style.fillStyle;
          context.fill(path);
        }
        context.restore();
      }
    }
  
    /**
     * @param {!CanvasRenderingContext2D} context
     */
    _drawEventDividers(context) {
      context.save();
      context.lineWidth = 1;
      for (const color of this._eventDividers.keys()) {
        context.strokeStyle = color;
        for (const time of this._eventDividers.get(color)) {
          context.beginPath();
          const x = this._timeToPosition(time);
          context.moveTo(x, 0);
          context.lineTo(x, this._offsetHeight);
        }
        context.stroke();
      }
      context.restore();
    }
  
    /**
     * @param {!RequestTimeRangeNames=} type
     * @return {number}
     */
    _getBarHeight(type) {
      const types = RequestTimeRangeNames;
      switch (type) {
        case types.Connecting:
        case types.SSL:
        case types.DNS:
        case types.Proxy:
        case types.Blocking:
        case types.Push:
        case types.Queueing:
          return 7;
        default:
          return 13;
      }
    }
  
    /**
     * @param {!SDK.NetworkRequest.NetworkRequest} request
     * @param {number} borderOffset
     * @return {!{start: number, mid: number, end: number}}
     */
    _getSimplifiedBarRange(request, borderOffset) {
      const drawWidth = this._offsetWidth - this._leftPadding;
      const percentages = this._calculator.computeBarGraphPercentages(request);
      return {
        start: this._leftPadding + Math.floor((percentages.start / 100) * drawWidth) + borderOffset,
        mid: this._leftPadding + Math.floor((percentages.middle / 100) * drawWidth) + borderOffset,
        end: this._leftPadding + Math.floor((percentages.end / 100) * drawWidth) + borderOffset
      };
    }
  
    /**
     * @param {!CanvasRenderingContext2D} context
     * @param {!NetworkNode} node
     * @param {number} y
     */
    _buildSimplifiedBarLayers(context, node, y) {
      const request = node.request();
      if (!request) {
        return;
      }
      const borderWidth = 1;
      const borderOffset = borderWidth % 2 === 0 ? 0 : 0.5;
  
      const ranges = this._getSimplifiedBarRange(request, borderOffset);
      const height = this._getBarHeight();
      y += Math.floor(this._rowHeight / 2 - height / 2 + borderWidth) - borderWidth / 2;
  
      const waitingStyle = this._styleForWaitingResourceType.get(request.resourceType());
      const waitingPath = this._pathForStyle.get(waitingStyle);
      waitingPath.rect(ranges.start, y, ranges.mid - ranges.start, height - borderWidth);
  
      const barWidth = Math.max(2, ranges.end - ranges.mid);
      const downloadingStyle = this._styleForDownloadingResourceType.get(request.resourceType());
      const downloadingPath = this._pathForStyle.get(downloadingStyle);
      downloadingPath.rect(ranges.mid, y, barWidth, height - borderWidth);
  
      /** @type {?{left: string, right: string, tooltip: (string|undefined)}} */
      let labels = null;
      if (node.hovered()) {
        labels = this._calculator.computeBarGraphLabels(request);
        const barDotLineLength = 10;
        const leftLabelWidth = context.measureText(labels.left).width;
        const rightLabelWidth = context.measureText(labels.right).width;
        const hoverLinePath = this._pathForStyle.get(this._hoverDetailsStyle);
  
        if (leftLabelWidth < ranges.mid - ranges.start) {
          const midBarX = ranges.start + (ranges.mid - ranges.start - leftLabelWidth) / 2;
          this._textLayers.push({text: labels.left, x: midBarX, y: y + this._fontSize});
        } else if (barDotLineLength + leftLabelWidth + this._leftPadding < ranges.start) {
          this._textLayers.push(
              {text: labels.left, x: ranges.start - leftLabelWidth - barDotLineLength - 1, y: y + this._fontSize});
          hoverLinePath.moveTo(ranges.start - barDotLineLength, y + Math.floor(height / 2));
          hoverLinePath.arc(ranges.start, y + Math.floor(height / 2), 2, 0, 2 * Math.PI);
          hoverLinePath.moveTo(ranges.start - barDotLineLength, y + Math.floor(height / 2));
          hoverLinePath.lineTo(ranges.start, y + Math.floor(height / 2));
        }
  
        const endX = ranges.mid + barWidth + borderOffset;
        if (rightLabelWidth < endX - ranges.mid) {
          const midBarX = ranges.mid + (endX - ranges.mid - rightLabelWidth) / 2;
          this._textLayers.push({text: labels.right, x: midBarX, y: y + this._fontSize});
        } else if (endX + barDotLineLength + rightLabelWidth < this._offsetWidth - this._leftPadding) {
          this._textLayers.push({text: labels.right, x: endX + barDotLineLength + 1, y: y + this._fontSize});
          hoverLinePath.moveTo(endX, y + Math.floor(height / 2));
          hoverLinePath.arc(endX, y + Math.floor(height / 2), 2, 0, 2 * Math.PI);
          hoverLinePath.moveTo(endX, y + Math.floor(height / 2));
          hoverLinePath.lineTo(endX + barDotLineLength, y + Math.floor(height / 2));
        }
      }
  
      if (!this._calculator.startAtZero) {
        const queueingRange = RequestTimingView.calculateRequestTimeRanges(request, 0)
                                  .find(data => data.name === RequestTimeRangeNames.Total);
        const leftLabelWidth = labels ? context.measureText(labels.left).width : 0;
        const leftTextPlacedInBar = leftLabelWidth < ranges.mid - ranges.start;
        const wiskerTextPadding = 13;
        const textOffset = (labels && !leftTextPlacedInBar) ? leftLabelWidth + wiskerTextPadding : 0;
        const queueingStart = this._timeToPosition(queueingRange.start);
        if (ranges.start - textOffset > queueingStart) {
          const wiskerPath = this._pathForStyle.get(this._wiskerStyle);
          wiskerPath.moveTo(queueingStart, y + Math.floor(height / 2));
          wiskerPath.lineTo(ranges.start - textOffset, y + Math.floor(height / 2));
  
          // TODO(allada) This needs to be floored.
          const wiskerHeight = height / 2;
          wiskerPath.moveTo(queueingStart + borderOffset, y + wiskerHeight / 2);
          wiskerPath.lineTo(queueingStart + borderOffset, y + height - wiskerHeight / 2 - 1);
        }
      }
    }
  
    /**
     * @param {!NetworkNode} node
     * @param {number} y
     */
    _buildTimingBarLayers(node, y) {
      const request = node.request();
      if (!request) {
        return;
      }
      const ranges = RequestTimingView.calculateRequestTimeRanges(request, 0);
      for (const range of ranges) {
        if (range.name === RequestTimeRangeNames.Total || range.name === RequestTimeRangeNames.Sending ||
            range.end - range.start === 0) {
          continue;
        }
  
        const style = this._styleForTimeRangeName.get(range.name);
        const path = this._pathForStyle.get(style);
        const lineWidth = style.lineWidth || 0;
        const height = this._getBarHeight(range.name);
        const middleBarY = y + Math.floor(this._rowHeight / 2 - height / 2) + lineWidth / 2;
        const start = this._timeToPosition(range.start);
        const end = this._timeToPosition(range.end);
        path.rect(start, middleBarY, end - start, height - lineWidth);
      }
    }
  
    /**
     * @param {!CanvasRenderingContext2D} context
     * @param {!NetworkNode} node
     * @param {number} y
     */
    _decorateRow(context, node, y) {
      const nodeBgColorId = node.backgroundColor();
      context.save();
      context.beginPath();
      context.fillStyle = self.UI.themeSupport.getComputedValue(nodeBgColorId);
      context.rect(0, y, this._offsetWidth, this._rowHeight);
      context.fill();
      context.restore();
    }
  }
  
  /** @typedef {!{x: number, y: number, text: string}} */
  export let _TextLayer;
  
  /** @typedef {!{fillStyle: (string|undefined), lineWidth: (number|undefined), borderColor: (string|undefined)}} */
  export let _LayerStyle;
  