| // 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(); |
| } |