UNPKG

chrome-devtools-frontend

Version:
490 lines (412 loc) • 15.6 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 {type AreaBounds, type Bounds} from './common.js'; import {drawGridLabels, type GridLabelState} from './css_grid_label_helpers.js'; import {applyMatrixToPoint, buildPath, emptyBounds, hatchFillPath} from './highlight_common.js'; // TODO(alexrudenko): Grid label unit tests depend on this style so it cannot be extracted yet. export const gridStyle = ` /* Grid row and column labels */ .grid-label-content { position: absolute; -webkit-user-select: none; padding: 2px; font-family: Menlo, monospace; font-size: 10px; min-width: 17px; min-height: 15px; border-radius: 2px; box-sizing: border-box; z-index: 1; background-clip: padding-box; pointer-events: none; text-align: center; display: flex; justify-content: center; align-items: center; } .grid-label-content[data-direction=row] { background-color: var(--row-label-color, #1A73E8); color: var(--row-label-text-color, #121212); } .grid-label-content[data-direction=column] { background-color: var(--column-label-color, #1A73E8); color: var(--column-label-text-color,#121212); } .line-names ul, .line-names .line-name { margin: 0; padding: 0; list-style: none; } .line-names .line-name { max-width: 100px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .line-names .grid-label-content, .line-numbers .grid-label-content, .track-sizes .grid-label-content { border: 1px solid white; --inner-corner-avoid-distance: 15px; } .grid-label-content.top-left.inner-shared-corner, .grid-label-content.top-right.inner-shared-corner { transform: translateY(var(--inner-corner-avoid-distance)); } .grid-label-content.bottom-left.inner-shared-corner, .grid-label-content.bottom-right.inner-shared-corner { transform: translateY(calc(var(--inner-corner-avoid-distance) * -1)); } .grid-label-content.left-top.inner-shared-corner, .grid-label-content.left-bottom.inner-shared-corner { transform: translateX(var(--inner-corner-avoid-distance)); } .grid-label-content.right-top.inner-shared-corner, .grid-label-content.right-bottom.inner-shared-corner { transform: translateX(calc(var(--inner-corner-avoid-distance) * -1)); } .line-names .grid-label-content::before, .line-numbers .grid-label-content::before, .track-sizes .grid-label-content::before { position: absolute; z-index: 1; pointer-events: none; content: ""; width: 3px; height: 3px; border: 1px solid white; border-width: 0 1px 1px 0; } .line-names .grid-label-content[data-direction=row]::before, .line-numbers .grid-label-content[data-direction=row]::before, .track-sizes .grid-label-content[data-direction=row]::before { background: var(--row-label-color, #1A73E8); } .line-names .grid-label-content[data-direction=column]::before, .line-numbers .grid-label-content[data-direction=column]::before, .track-sizes .grid-label-content[data-direction=column]::before { background: var(--column-label-color, #1A73E8); } .grid-label-content.bottom-mid::before { transform: translateY(-1px) rotate(45deg); top: 100%; } .grid-label-content.top-mid::before { transform: translateY(-3px) rotate(-135deg); top: 0%; } .grid-label-content.left-mid::before { transform: translateX(-3px) rotate(135deg); left: 0% } .grid-label-content.right-mid::before { transform: translateX(3px) rotate(-45deg); right: 0%; } .grid-label-content.right-top::before { transform: translateX(3px) translateY(-1px) rotate(-90deg) skewY(30deg); right: 0%; top: 0%; } .grid-label-content.right-bottom::before { transform: translateX(3px) translateY(-3px) skewX(30deg); right: 0%; top: 100%; } .grid-label-content.bottom-right::before { transform: translateX(1px) translateY(-1px) skewY(30deg); right: 0%; top: 100%; } .grid-label-content.bottom-left::before { transform: translateX(-1px) translateY(-1px) rotate(90deg) skewX(30deg); left: 0%; top: 100%; } .grid-label-content.left-top::before { transform: translateX(-3px) translateY(-1px) rotate(180deg) skewX(30deg); left: 0%; top: 0%; } .grid-label-content.left-bottom::before { transform: translateX(-3px) translateY(-3px) rotate(90deg) skewY(30deg); left: 0%; top: 100%; } .grid-label-content.top-right::before { transform: translateX(1px) translateY(-3px) rotate(-90deg) skewX(30deg); right: 0%; top: 0%; } .grid-label-content.top-left::before { transform: translateX(-1px) translateY(-3px) rotate(180deg) skewY(30deg); left: 0%; top: 0%; } @media (forced-colors: active) { .grid-label-content { border-color: Highlight; background-color: Canvas; color: Text; forced-color-adjust: none; } .grid-label-content::before { background-color: Canvas; border-color: Highlight; } }`; export interface GridHighlight { gridBorder: Array<string|number>; writingMode: string; rowGaps: Array<string|number>; rotationAngle: number; columnGaps: Array<string|number>; rows: Array<string|number>; columns: Array<string|number>; areaNames: {[key: string]: Array<string|number>}; gridHighlightConfig: { gridBackgroundColor?: string, gridBorderColor?: string, rowGapColor?: string, columnGapColor?: string, areaBorderColor?: string, gridBorderDash: boolean, rowLineDash: boolean, columnLineDash: boolean, showGridExtensionLines: boolean, showPositiveLineNumbers: boolean, showNegativeLineNumbers: boolean, rowLineColor: string, columnLineColor: string, rowHatchColor: string, columnHatchColor: string, showLineNames: boolean, }; } export function drawLayoutGridHighlight( highlight: GridHighlight, context: CanvasRenderingContext2D, deviceScaleFactor: number, canvasWidth: number, canvasHeight: number, emulationScaleFactor: number, labelState: GridLabelState) { const gridBounds = emptyBounds(); const gridPath = buildPath(highlight.gridBorder, gridBounds, emulationScaleFactor); // Transform the context to match the current writing-mode. context.save(); applyWritingModeTransformation(highlight.writingMode, gridBounds, context); // Draw grid background if (highlight.gridHighlightConfig.gridBackgroundColor) { context.fillStyle = highlight.gridHighlightConfig.gridBackgroundColor; context.fill(gridPath); } // Draw Grid border if (highlight.gridHighlightConfig.gridBorderColor) { context.save(); context.translate(0.5, 0.5); context.lineWidth = 0; if (highlight.gridHighlightConfig.gridBorderDash) { context.setLineDash([3, 3]); } context.strokeStyle = highlight.gridHighlightConfig.gridBorderColor; context.stroke(gridPath); context.restore(); } // Draw grid lines const rowBounds = drawGridLines(context, highlight, 'row', emulationScaleFactor); const columnBounds = drawGridLines(context, highlight, 'column', emulationScaleFactor); // Draw gaps drawGridGap( context, highlight.rowGaps, highlight.gridHighlightConfig.rowGapColor, highlight.gridHighlightConfig.rowHatchColor, highlight.rotationAngle, emulationScaleFactor, /* flipDirection */ true); drawGridGap( context, highlight.columnGaps, highlight.gridHighlightConfig.columnGapColor, highlight.gridHighlightConfig.columnHatchColor, highlight.rotationAngle, emulationScaleFactor, /* flipDirection */ false); // Draw named grid areas const areaBounds = drawGridAreas(context, highlight.areaNames, highlight.gridHighlightConfig.areaBorderColor, emulationScaleFactor); // The rest of the overlay is drawn without the writing-mode transformation, but we keep the matrix to transform relevant points. const writingModeMatrix = context.getTransform(); writingModeMatrix.scaleSelf(1 / deviceScaleFactor); context.restore(); if (highlight.gridHighlightConfig.showGridExtensionLines) { if (rowBounds) { drawExtendedGridLines( context, rowBounds, highlight.gridHighlightConfig.rowLineColor, highlight.gridHighlightConfig.rowLineDash, writingModeMatrix, canvasWidth, canvasHeight); } if (columnBounds) { drawExtendedGridLines( context, columnBounds, highlight.gridHighlightConfig.columnLineColor, highlight.gridHighlightConfig.columnLineDash, writingModeMatrix, canvasWidth, canvasHeight); } } // Draw all the labels drawGridLabels( highlight, gridBounds, areaBounds, {canvasWidth, canvasHeight}, labelState, emulationScaleFactor, writingModeMatrix); } function applyWritingModeTransformation(writingMode: string, gridBounds: Bounds, context: CanvasRenderingContext2D) { if (writingMode !== 'vertical-rl' && writingMode !== 'vertical-lr') { return; } const topLeft = gridBounds.allPoints[0]; const bottomLeft = gridBounds.allPoints[3]; // Move to the top-left corner to do all transformations there. context.translate(topLeft.x, topLeft.y); if (writingMode === 'vertical-rl') { context.rotate(90 * Math.PI / 180); context.translate(0, -1 * (bottomLeft.y - topLeft.y)); } if (writingMode === 'vertical-lr') { context.rotate(90 * Math.PI / 180); context.scale(1, -1); } // Move back to the original point. context.translate(topLeft.x * -1, topLeft.y * -1); } function drawGridLines( context: CanvasRenderingContext2D, highlight: GridHighlight, direction: 'row'|'column', emulationScaleFactor: number) { const tracks = highlight[`${direction}s` as 'rows' | 'columns']; const color = highlight.gridHighlightConfig[`${direction}LineColor` as 'rowLineColor' | 'columnLineColor']; const dash = highlight.gridHighlightConfig[`${direction}LineDash` as 'rowLineDash' | 'columnLineDash']; if (!color) { return null; } const bounds = emptyBounds(); const path = buildPath(tracks, bounds, emulationScaleFactor); context.save(); context.translate(0.5, 0.5); if (dash) { context.setLineDash([3, 3]); } context.lineWidth = 0; context.strokeStyle = color; context.save(); context.stroke(path); context.restore(); context.restore(); return bounds; } function drawExtendedGridLines( context: CanvasRenderingContext2D, bounds: Bounds, color: string, dash: boolean|undefined, writingModeMatrix: DOMMatrix, canvasWidth: number, canvasHeight: number) { context.save(); context.strokeStyle = color; context.lineWidth = 1; context.translate(0.5, 0.5); if (dash) { context.setLineDash([3, 3]); } // A grid track path is a list of lines defined by 2 points. // Here we're going through the list of all points 2 by 2, so we can draw the extensions at the edges of each line. for (let i = 0; i < bounds.allPoints.length; i += 2) { let point1 = applyMatrixToPoint(bounds.allPoints[i], writingModeMatrix); let point2 = applyMatrixToPoint(bounds.allPoints[i + 1], writingModeMatrix); let edgePoint1; let edgePoint2; if (point1.x === point2.x) { // Special case for a vertical line. edgePoint1 = {x: point1.x, y: 0}; edgePoint2 = {x: point1.x, y: canvasHeight}; if (point2.y < point1.y) { [point1, point2] = [point2, point1]; } } else if (point1.y === point2.y) { // Special case for a horizontal line. edgePoint1 = {x: 0, y: point1.y}; edgePoint2 = {x: canvasWidth, y: point1.y}; if (point2.x < point1.x) { [point1, point2] = [point2, point1]; } } else { // When the line isn't straight, we need to do some maths. const a = (point2.y - point1.y) / (point2.x - point1.x); const b = (point1.y * point2.x - point2.y * point1.x) / (point2.x - point1.x); edgePoint1 = {x: 0, y: b}; edgePoint2 = {x: canvasWidth, y: (canvasWidth * a) + b}; if (point2.x < point1.x) { [point1, point2] = [point2, point1]; } } context.beginPath(); context.moveTo(edgePoint1.x, edgePoint1.y); context.lineTo(point1.x, point1.y); context.moveTo(point2.x, point2.y); context.lineTo(edgePoint2.x, edgePoint2.y); context.stroke(); } context.restore(); } /** * Draw all of the named grid area paths. This does not draw the labels, as * placing labels in and around the grid for various things is handled later. */ function drawGridAreas( context: CanvasRenderingContext2D, areas: {[key: string]: Array<string|number>}, borderColor: string|undefined, emulationScaleFactor: number): AreaBounds[] { if (!areas || !Object.keys(areas).length) { return []; } context.save(); if (borderColor) { context.strokeStyle = borderColor; } context.lineWidth = 2; const areaBounds = []; for (const name in areas) { const areaCommands = areas[name]; const bounds = emptyBounds(); const path = buildPath(areaCommands, bounds, emulationScaleFactor); context.stroke(path); areaBounds.push({name, bounds}); } context.restore(); return areaBounds; } function drawGridGap( context: CanvasRenderingContext2D, gapCommands: Array<number|string>, gapColor: string|undefined, hatchColor: string|undefined, rotationAngle: number, emulationScaleFactor: number, flipDirection: boolean|undefined) { if (!gapColor && !hatchColor) { return; } context.save(); context.translate(0.5, 0.5); context.lineWidth = 0; const bounds = emptyBounds(); const path = buildPath(gapCommands, bounds, emulationScaleFactor); // Fill the gap background if needed. if (gapColor) { context.fillStyle = gapColor; context.fill(path); } // And draw the hatch pattern if needed. if (hatchColor) { hatchFillPath(context, path, bounds, /* delta */ 10, hatchColor, rotationAngle, flipDirection); } context.restore(); }