UNPKG

chrome-devtools-frontend

Version:
281 lines (249 loc) • 10.8 kB
// 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 {createChild, Overlay, type Bounds, type ResetData} from './common.js'; import {buildPath, emptyBounds, type PathBounds} 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; override reset(resetData: ResetData) { super.reset(resetData); this.sourceOrderContainer.textContent = ''; } override 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(); } override 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; }