mirror of
https://github.com/react/react-native-devtools-frontend.git
synced 2026-10-07 04:26:32 +08:00
Bug: none Change-Id: I02ef1182143b7fe091ce92d59f04ac2188903e0d Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/2886925 Commit-Queue: Jack Franklin <jacktfranklin@chromium.org> Auto-Submit: Jack Franklin <jacktfranklin@chromium.org> Reviewed-by: Tim van der Lippe <tvanderlippe@chromium.org> Reviewed-by: Mathias Bynens <mathias@chromium.org>
282 lines
11 KiB
TypeScript
282 lines
11 KiB
TypeScript
// Copyright 2020 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.
|
|
|
|
// Copyright (C) 2012 Google Inc. All rights reserved.
|
|
|
|
// Redistribution and use in source and binary forms, with or without
|
|
// modification, are permitted provided that the following conditions
|
|
// are met:
|
|
|
|
// 1. Redistributions of source code must retain the above copyright
|
|
// notice, this list of conditions and the following disclaimer.
|
|
// 2. Redistributions in binary form must reproduce the above copyright
|
|
// notice, this list of conditions and the following disclaimer in the
|
|
// documentation and/or other materials provided with the distribution.
|
|
// 3. Neither the name of Apple Computer, Inc. ("Apple") nor the names of
|
|
// its contributors may be used to endorse or promote products derived
|
|
// from this software without specific prior written permission.
|
|
|
|
// THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
|
|
// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
|
// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
// DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
|
|
// DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
|
// (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
|
// LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
|
// ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
|
|
// THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
|
|
import type {Bounds, ResetData} from './common.js';
|
|
import {createChild, Overlay} from './common.js';
|
|
import type {PathBounds} from './highlight_common.js';
|
|
import {buildPath, emptyBounds} from './highlight_common.js';
|
|
|
|
interface Path {
|
|
path: Array<string|number>;
|
|
outlineColor: string;
|
|
name: string;
|
|
}
|
|
|
|
interface SourceOrderHighlight {
|
|
sourceOrder: number;
|
|
paths: Path[];
|
|
}
|
|
|
|
export class SourceOrderOverlay extends Overlay {
|
|
private sourceOrderContainer!: HTMLElement;
|
|
|
|
reset(resetData: ResetData) {
|
|
super.reset(resetData);
|
|
this.sourceOrderContainer.textContent = '';
|
|
}
|
|
|
|
install() {
|
|
this.document.body.classList.add('fill');
|
|
|
|
const canvas = this.document.createElement('canvas');
|
|
canvas.id = 'canvas';
|
|
canvas.classList.add('fill');
|
|
this.document.body.append(canvas);
|
|
|
|
const sourceOrderContainer = this.document.createElement('div');
|
|
sourceOrderContainer.id = 'source-order-container';
|
|
this.document.body.append(sourceOrderContainer);
|
|
this.sourceOrderContainer = sourceOrderContainer;
|
|
|
|
this.setCanvas(canvas);
|
|
|
|
super.install();
|
|
}
|
|
|
|
uninstall() {
|
|
this.document.body.classList.remove('fill');
|
|
this.document.body.innerHTML = '';
|
|
super.uninstall();
|
|
}
|
|
|
|
drawSourceOrder(highlight: SourceOrderHighlight) {
|
|
const sourceOrder = highlight.sourceOrder || 0;
|
|
const path = highlight.paths.slice().pop();
|
|
|
|
if (!path) {
|
|
throw new Error('No path provided');
|
|
}
|
|
|
|
this.context.save();
|
|
const bounds = emptyBounds();
|
|
const outlineColor = path.outlineColor;
|
|
|
|
this.context.save();
|
|
drawPath(this.context, path.path, outlineColor, Boolean(sourceOrder), bounds, this.emulationScaleFactor);
|
|
this.context.restore();
|
|
|
|
this.context.save();
|
|
if (Boolean(sourceOrder)) {
|
|
this.drawSourceOrderLabel(sourceOrder, outlineColor, bounds);
|
|
}
|
|
this.context.restore();
|
|
|
|
return {bounds: bounds};
|
|
}
|
|
|
|
private drawSourceOrderLabel(sourceOrder: number, color: string, bounds: PathBounds) {
|
|
const sourceOrderContainer = this.sourceOrderContainer;
|
|
const otherLabels = sourceOrderContainer.children;
|
|
const labelContainer = createChild(sourceOrderContainer, 'div', 'source-order-label-container');
|
|
labelContainer.style.color = color;
|
|
labelContainer.textContent = String(sourceOrder);
|
|
|
|
const labelHeight = labelContainer.offsetHeight;
|
|
const labelWidth = labelContainer.offsetWidth;
|
|
const labelType =
|
|
getLabelType(bounds, labelHeight, labelWidth, otherLabels as HTMLCollectionOf<HTMLElement>, this.canvasHeight);
|
|
const labelPosition = getPositionFromLabelType(labelType, bounds, labelHeight);
|
|
|
|
labelContainer.classList.add(labelType);
|
|
labelContainer.style.top = labelPosition.contentTop + 'px';
|
|
labelContainer.style.left = labelPosition.contentLeft + 'px';
|
|
}
|
|
}
|
|
|
|
// If there is a large number of child elements, labels will be placed in the top
|
|
// corner in order to keep the overlay rendering quick
|
|
const MAX_CHILD_ELEMENTS_THRESHOLD = 300;
|
|
|
|
/**
|
|
* There are 8 types of labels.
|
|
*
|
|
* There are 4 positions a label can have on the y axis, relative to the element:
|
|
* - topCorner, bottomCorner: placed inside of the element, in its left corners
|
|
* - aboveElement, belowElement: placed outside of the element, aligned on the
|
|
* left edge of the element
|
|
*
|
|
* The label position is determined as follows:
|
|
* 1. Top corner if the element is wider and taller than the element
|
|
* 2. Above element if the label is wider or taller than the element
|
|
* 3. Below element if the label would be placed above the element, but this would
|
|
* cause it to overlap with another label or intersect the top of the window
|
|
* 4. Bottom corner if the label would be placed below the element, but this would
|
|
* cause it to intersect the bottom of the window
|
|
* On the x axis, the label is always aligned with its element's leftmost edge.
|
|
*
|
|
* The label may need additional styles if it is taller or wider than the element,
|
|
* to make sure all borders that don't touch the element's outline are rounded
|
|
* - Wider: right corners are rounded
|
|
* - Taller: top corners are rounded
|
|
*
|
|
* Examples: (E = element, L = label)
|
|
* ______
|
|
* |_L_| | (the bottom right corner of the label will be rounded)
|
|
* topCorner: | |
|
|
* |___E__|
|
|
* ___
|
|
* |_L_| (the bottom right corner of the label will be rounded)
|
|
* aboveElementWider: | |
|
|
* |E|
|
|
* |_|
|
|
* ______
|
|
* bottomCornerTaller: | L |_____
|
|
* |______|__E__|
|
|
*/
|
|
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
export const LabelTypes = {
|
|
topCorner: 'top-corner',
|
|
aboveElement: 'above-element',
|
|
belowElement: 'below-element',
|
|
aboveElementWider: 'above-element-wider',
|
|
belowElementWider: 'below-element-wider',
|
|
bottomCornerWider: 'bottom-corner-wider',
|
|
bottomCornerTaller: 'bottom-corner-taller',
|
|
bottomCornerWiderTaller: 'bottom-corner-wider-taller',
|
|
};
|
|
|
|
/**
|
|
* Calculates the coordinates to place the label based on position type
|
|
*/
|
|
export function getPositionFromLabelType(positionType: string, bounds: Omit<Bounds, 'allPoints'>, labelHeight: number):
|
|
{contentTop: number, contentLeft: number} {
|
|
let contentTop = 0;
|
|
switch (positionType) {
|
|
case LabelTypes.topCorner:
|
|
contentTop = bounds.minY;
|
|
break;
|
|
case LabelTypes.aboveElement:
|
|
case LabelTypes.aboveElementWider:
|
|
contentTop = bounds.minY - labelHeight;
|
|
break;
|
|
case LabelTypes.belowElement:
|
|
case LabelTypes.belowElementWider:
|
|
contentTop = bounds.maxY;
|
|
break;
|
|
case LabelTypes.bottomCornerWider:
|
|
case LabelTypes.bottomCornerTaller:
|
|
case LabelTypes.bottomCornerWiderTaller:
|
|
contentTop = bounds.maxY - labelHeight;
|
|
break;
|
|
}
|
|
return {
|
|
contentTop,
|
|
contentLeft: bounds.minX,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Determines the position type of the label based on the element it's associated
|
|
* with, avoiding overlaps between other labels
|
|
*/
|
|
export function getLabelType(
|
|
bounds: Omit<Bounds, 'allPoints'>, labelHeight: number, labelWidth: number,
|
|
otherLabels: HTMLCollectionOf<HTMLElement>, canvasHeight: number): string {
|
|
let labelType;
|
|
// Label goes in the top left corner if the element is bigger than the label
|
|
// or if there are too many child nodes
|
|
const widerThanElement = bounds.minX + labelWidth > bounds.maxX;
|
|
const tallerThanElement = bounds.minY + labelHeight > bounds.maxY;
|
|
if ((!widerThanElement && !tallerThanElement) || otherLabels.length >= MAX_CHILD_ELEMENTS_THRESHOLD) {
|
|
return LabelTypes.topCorner;
|
|
}
|
|
|
|
// Check if the new label would overlap with an existing label if placed above
|
|
// its element
|
|
let overlaps = false;
|
|
for (let i = 0; i < otherLabels.length; i++) {
|
|
const currentLabel = otherLabels[i];
|
|
const rect = currentLabel.getBoundingClientRect();
|
|
// Skip the newly created/appended element that is currently being placed
|
|
if (currentLabel.style.top === '' && currentLabel.style.left === '') {
|
|
continue;
|
|
}
|
|
const topOverlaps = bounds.minY - labelHeight <= rect.top + rect.height && bounds.minY - labelHeight >= rect.top;
|
|
const bottomOverlaps = bounds.minY <= rect.top + rect.height && bounds.minY >= rect.top;
|
|
const leftOverlaps = bounds.minX >= rect.left && bounds.minX <= rect.left + rect.width;
|
|
const rightOverlaps = bounds.minX + labelWidth >= rect.left && bounds.minX + labelWidth <= rect.left + rect.width;
|
|
const sideOverlaps = leftOverlaps || rightOverlaps;
|
|
if (sideOverlaps && (topOverlaps || bottomOverlaps)) {
|
|
overlaps = true;
|
|
break;
|
|
}
|
|
}
|
|
// Label goes on top of the element if the element is too small
|
|
if (bounds.minY - labelHeight > 0 && !overlaps) {
|
|
labelType = LabelTypes.aboveElement;
|
|
if (widerThanElement) {
|
|
labelType = LabelTypes.aboveElementWider;
|
|
}
|
|
// Label goes below the element if would go off the screen/overlap with another label
|
|
} else if (bounds.maxY + labelHeight < canvasHeight) {
|
|
labelType = LabelTypes.belowElement;
|
|
if (widerThanElement) {
|
|
labelType = LabelTypes.belowElementWider;
|
|
}
|
|
// Label goes in the bottom left corner of the element if putting it below the element would make it go off the screen
|
|
} else {
|
|
if (widerThanElement && tallerThanElement) {
|
|
labelType = LabelTypes.bottomCornerWiderTaller;
|
|
} else if (widerThanElement) {
|
|
labelType = LabelTypes.bottomCornerWider;
|
|
} else {
|
|
labelType = LabelTypes.bottomCornerTaller;
|
|
}
|
|
}
|
|
return labelType;
|
|
}
|
|
|
|
function drawPath(
|
|
context: CanvasRenderingContext2D, commands: Array<string|number>, outlineColor: string|undefined, isChild: boolean,
|
|
bounds: PathBounds, emulationScaleFactor: number): Path2D {
|
|
context.save();
|
|
const path = buildPath(commands, bounds, emulationScaleFactor);
|
|
if (outlineColor) {
|
|
context.strokeStyle = outlineColor;
|
|
context.lineWidth = 2;
|
|
if (!isChild) {
|
|
context.setLineDash([3, 3]);
|
|
}
|
|
context.stroke(path);
|
|
}
|
|
context.restore();
|
|
return path;
|
|
}
|