1// Copyright 2020 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5//  Copyright (C) 2012 Google Inc. All rights reserved.
6
7//  Redistribution and use in source and binary forms, with or without
8//  modification, are permitted provided that the following conditions
9//  are met:
10
11//  1.  Redistributions of source code must retain the above copyright
12//      notice, this list of conditions and the following disclaimer.
13//  2.  Redistributions in binary form must reproduce the above copyright
14//      notice, this list of conditions and the following disclaimer in the
15//      documentation and/or other materials provided with the distribution.
16//  3.  Neither the name of Apple Computer, Inc. ("Apple") nor the names of
17//      its contributors may be used to endorse or promote products derived
18//      from this software without specific prior written permission.
19
20//  THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
21//  EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
22//  WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
23//  DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
24//  DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
25//  (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
26//  LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
27//  ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
28//  (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
29//  THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30
31
32import {AreaBounds, Bounds} from './common.js';
33import {drawGridLabels, GridLabelState} from './css_grid_label_helpers.js';
34import {applyMatrixToPoint, buildPath, emptyBounds} from './highlight_common.js';
35
36const DEFAULT_EXTENDED_LINE_COLOR = 'rgba(128, 128, 128, 0.3)';
37
38// TODO(alexrudenko): Grid label unit tests depend on this style so it cannot be extracted yet.
39export const gridStyle = `
40/* Grid row and column labels */
41.grid-label-content {
42  position: absolute;
43  -webkit-user-select: none;
44  padding: 2px;
45  font-family: Menlo, monospace;
46  font-size: 10px;
47  min-width: 17px;
48  min-height: 15px;
49  border-radius: 2px;
50  box-sizing: border-box;
51  z-index: 1;
52  background-clip: padding-box;
53  pointer-events: none;
54  text-align: center;
55  display: flex;
56  justify-content: center;
57  align-items: center;
58}
59
60.grid-label-content[data-direction=row] {
61  background-color: var(--row-label-color, #1A73E8);
62  color: #121212;
63}
64
65.grid-label-content[data-direction=column] {
66  background-color: var(--column-label-color, #1A73E8);
67  color: #121212;
68}
69
70.line-names ul,
71.line-names .line-name {
72  margin: 0;
73  padding: 0;
74  list-style: none;
75}
76
77.line-names .line-name {
78  max-width: 100px;
79  white-space: nowrap;
80  overflow: hidden;
81  text-overflow: ellipsis;
82}
83
84.line-names .grid-label-content,
85.line-numbers .grid-label-content,
86.track-sizes .grid-label-content {
87  border: 1px solid white;
88  --inner-corner-avoid-distance: 15px;
89}
90
91.grid-label-content.top-left.inner-shared-corner,
92.grid-label-content.top-right.inner-shared-corner {
93  transform: translateY(var(--inner-corner-avoid-distance));
94}
95
96.grid-label-content.bottom-left.inner-shared-corner,
97.grid-label-content.bottom-right.inner-shared-corner {
98  transform: translateY(calc(var(--inner-corner-avoid-distance) * -1));
99}
100
101.grid-label-content.left-top.inner-shared-corner,
102.grid-label-content.left-bottom.inner-shared-corner {
103  transform: translateX(var(--inner-corner-avoid-distance));
104}
105
106.grid-label-content.right-top.inner-shared-corner,
107.grid-label-content.right-bottom.inner-shared-corner {
108  transform: translateX(calc(var(--inner-corner-avoid-distance) * -1));
109}
110
111.line-names .grid-label-content::before,
112.line-numbers .grid-label-content::before,
113.track-sizes .grid-label-content::before {
114  position: absolute;
115  z-index: 1;
116  pointer-events: none;
117  content: "";
118  width: 3px;
119  height: 3px;
120  border: 1px solid white;
121  border-width: 0 1px 1px 0;
122}
123
124.line-names .grid-label-content[data-direction=row]::before,
125.line-numbers .grid-label-content[data-direction=row]::before,
126.track-sizes .grid-label-content[data-direction=row]::before {
127  background: var(--row-label-color, #1A73E8);
128}
129
130.line-names .grid-label-content[data-direction=column]::before,
131.line-numbers .grid-label-content[data-direction=column]::before,
132.track-sizes .grid-label-content[data-direction=column]::before {
133  background: var(--column-label-color, #1A73E8);
134}
135
136.grid-label-content.bottom-mid::before {
137  transform: translateY(-1px) rotate(45deg);
138  top: 100%;
139}
140
141.grid-label-content.top-mid::before {
142  transform: translateY(-3px) rotate(-135deg);
143  top: 0%;
144}
145
146.grid-label-content.left-mid::before {
147  transform: translateX(-3px) rotate(135deg);
148  left: 0%
149}
150
151.grid-label-content.right-mid::before {
152  transform: translateX(3px) rotate(-45deg);
153  right: 0%;
154}
155
156.grid-label-content.right-top::before {
157  transform: translateX(3px) translateY(-1px) rotate(-90deg) skewY(30deg);
158  right: 0%;
159  top: 0%;
160}
161
162.grid-label-content.right-bottom::before {
163  transform: translateX(3px) translateY(-3px) skewX(30deg);
164  right: 0%;
165  top: 100%;
166}
167
168.grid-label-content.bottom-right::before {
169  transform:  translateX(1px) translateY(-1px) skewY(30deg);
170  right: 0%;
171  top: 100%;
172}
173
174.grid-label-content.bottom-left::before {
175  transform:  translateX(-1px) translateY(-1px) rotate(90deg) skewX(30deg);
176  left: 0%;
177  top: 100%;
178}
179
180.grid-label-content.left-top::before {
181  transform: translateX(-3px) translateY(-1px) rotate(180deg) skewX(30deg);
182  left: 0%;
183  top: 0%;
184}
185
186.grid-label-content.left-bottom::before {
187  transform: translateX(-3px) translateY(-3px) rotate(90deg) skewY(30deg);
188  left: 0%;
189  top: 100%;
190}
191
192.grid-label-content.top-right::before {
193  transform:  translateX(1px) translateY(-3px) rotate(-90deg) skewX(30deg);
194  right: 0%;
195  top: 0%;
196}
197
198.grid-label-content.top-left::before {
199  transform:  translateX(-1px) translateY(-3px) rotate(180deg) skewY(30deg);
200  left: 0%;
201  top: 0%;
202}
203
204@media (forced-colors: active) {
205  .grid-label-content {
206      border-color: Highlight;
207      background-color: Canvas;
208      color: Text;
209      forced-color-adjust: none;
210  }
211  .grid-label-content::before {
212    background-color: Canvas;
213    border-color: Highlight;
214  }
215}`;
216
217export interface GridHighlight {
218  gridBorder: Array<string|number>;
219  writingMode: string;
220  rowGaps: Array<string|number>;
221  rotationAngle: number, columnGaps: Array<string|number>;
222  rows: Array<string|number>;
223  columns: Array<string|number>;
224  areaNames: {[key: string]: Array<string|number>};
225  gridHighlightConfig: {
226    gridBackgroundColor?: string;
227    gridBorderColor?: string;
228    rowGapColor?: string;
229    columnGapColor?: string;
230    areaBorderColor?: string; gridBorderDash: boolean; rowLineDash: boolean; columnLineDash: boolean;
231    showGridExtensionLines: boolean;
232    showPositiveLineNumbers: boolean;
233    showNegativeLineNumbers: boolean;
234    rowLineColor: string;
235    columnLineColor: string;
236    rowHatchColor: string;
237    columnHatchColor: string;
238    showLineNames: boolean;
239  }
240}
241
242export function drawLayoutGridHighlight(
243    highlight: GridHighlight, context: CanvasRenderingContext2D, deviceScaleFactor: number, canvasWidth: number,
244    canvasHeight: number, emulationScaleFactor: number, labelState: GridLabelState) {
245  const gridBounds = emptyBounds();
246  const gridPath = buildPath(highlight.gridBorder, gridBounds, emulationScaleFactor);
247
248  // Transform the context to match the current writing-mode.
249  context.save();
250  _applyWritingModeTransformation(highlight.writingMode, gridBounds, context);
251
252  // Draw grid background
253  if (highlight.gridHighlightConfig.gridBackgroundColor) {
254    context.fillStyle = highlight.gridHighlightConfig.gridBackgroundColor;
255    context.fill(gridPath);
256  }
257
258  // Draw Grid border
259  if (highlight.gridHighlightConfig.gridBorderColor) {
260    context.save();
261    context.translate(0.5, 0.5);
262    context.lineWidth = 0;
263    if (highlight.gridHighlightConfig.gridBorderDash) {
264      context.setLineDash([3, 3]);
265    }
266    context.strokeStyle = highlight.gridHighlightConfig.gridBorderColor;
267    context.stroke(gridPath);
268    context.restore();
269  }
270
271  // Draw grid lines
272  const rowBounds = _drawGridLines(context, highlight, 'row', emulationScaleFactor);
273  const columnBounds = _drawGridLines(context, highlight, 'column', emulationScaleFactor);
274
275  // Draw gaps
276  _drawGridGap(
277      context, highlight.rowGaps, highlight.gridHighlightConfig.rowGapColor,
278      highlight.gridHighlightConfig.rowHatchColor, highlight.rotationAngle, emulationScaleFactor,
279      /* flipDirection */ true);
280  _drawGridGap(
281      context, highlight.columnGaps, highlight.gridHighlightConfig.columnGapColor,
282      highlight.gridHighlightConfig.columnHatchColor, highlight.rotationAngle, emulationScaleFactor,
283      /* flipDirection */ false);
284
285  // Draw named grid areas
286  const areaBounds =
287      _drawGridAreas(context, highlight.areaNames, highlight.gridHighlightConfig.areaBorderColor, emulationScaleFactor);
288
289  // The rest of the overlay is drawn without the writing-mode transformation, but we keep the matrix to transform relevant points.
290  const writingModeMatrix = context.getTransform();
291  writingModeMatrix.scaleSelf(1 / deviceScaleFactor);
292  context.restore();
293
294  if (highlight.gridHighlightConfig.showGridExtensionLines) {
295    if (rowBounds) {
296      _drawExtendedGridLines(
297          context, rowBounds, highlight.gridHighlightConfig.rowLineDash, writingModeMatrix, canvasWidth, canvasHeight);
298    }
299    if (columnBounds) {
300      _drawExtendedGridLines(
301          context, columnBounds, highlight.gridHighlightConfig.columnLineDash, writingModeMatrix, canvasWidth,
302          canvasHeight);
303    }
304  }
305
306  // Draw all the labels
307  drawGridLabels(highlight, gridBounds, areaBounds, {canvasWidth, canvasHeight}, labelState, writingModeMatrix);
308}
309
310function _applyWritingModeTransformation(writingMode: string, gridBounds: Bounds, context: CanvasRenderingContext2D) {
311  if (writingMode !== 'vertical-rl' && writingMode !== 'vertical-lr') {
312    return;
313  }
314
315  const topLeft = gridBounds.allPoints[0];
316  const bottomLeft = gridBounds.allPoints[3];
317
318  // Move to the top-left corner to do all transformations there.
319  context.translate(topLeft.x, topLeft.y);
320
321  if (writingMode === 'vertical-rl') {
322    context.rotate(90 * Math.PI / 180);
323    context.translate(0, -1 * (bottomLeft.y - topLeft.y));
324  }
325
326  if (writingMode === 'vertical-lr') {
327    context.rotate(90 * Math.PI / 180);
328    context.scale(1, -1);
329  }
330
331  // Move back to the original point.
332  context.translate(topLeft.x * -1, topLeft.y * -1);
333}
334
335function _drawGridLines(
336    context: CanvasRenderingContext2D, highlight: GridHighlight, direction: 'row'|'column',
337    emulationScaleFactor: number) {
338  const tracks = highlight[`${direction}s` as 'rows' | 'columns'];
339  const color = highlight.gridHighlightConfig[`${direction}LineColor` as 'rowLineColor' | 'columnLineColor'];
340  const dash = highlight.gridHighlightConfig[`${direction}LineDash` as 'rowLineDash' | 'columnLineDash'];
341
342  if (!color) {
343    return null;
344  }
345
346  const bounds = emptyBounds();
347  const path = buildPath(tracks, bounds, emulationScaleFactor);
348
349  context.save();
350  context.translate(0.5, 0.5);
351  if (dash) {
352    context.setLineDash([3, 3]);
353  }
354  context.lineWidth = 0;
355  context.strokeStyle = color;
356
357  context.save();
358  context.stroke(path);
359  context.restore();
360
361  context.restore();
362
363  return bounds;
364}
365
366function _drawExtendedGridLines(
367    context: CanvasRenderingContext2D, bounds: Bounds, dash: boolean|undefined, writingModeMatrix: DOMMatrix,
368    canvasWidth: number, canvasHeight: number) {
369  context.save();
370  context.strokeStyle = DEFAULT_EXTENDED_LINE_COLOR;
371  context.lineWidth = 1;
372  context.translate(0.5, 0.5);
373  if (dash) {
374    context.setLineDash([3, 3]);
375  }
376
377  // A grid track path is a list of lines defined by 2 points.
378  // 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.
379  for (let i = 0; i < bounds.allPoints.length; i += 2) {
380    let point1 = applyMatrixToPoint(bounds.allPoints[i], writingModeMatrix);
381    let point2 = applyMatrixToPoint(bounds.allPoints[i + 1], writingModeMatrix);
382    let edgePoint1;
383    let edgePoint2;
384
385    if (point1.x === point2.x) {
386      // Special case for a vertical line.
387      edgePoint1 = {x: point1.x, y: 0};
388      edgePoint2 = {x: point1.x, y: canvasHeight};
389      if (point2.y < point1.y) {
390        [point1, point2] = [point2, point1];
391      }
392    } else if (point1.y === point2.y) {
393      // Special case for a horizontal line.
394      edgePoint1 = {x: 0, y: point1.y};
395      edgePoint2 = {x: canvasWidth, y: point1.y};
396      if (point2.x < point1.x) {
397        [point1, point2] = [point2, point1];
398      }
399    } else {
400      // When the line isn't straight, we need to do some maths.
401      const a = (point2.y - point1.y) / (point2.x - point1.x);
402      const b = (point1.y * point2.x - point2.y * point1.x) / (point2.x - point1.x);
403
404      edgePoint1 = {x: 0, y: b};
405      edgePoint2 = {x: canvasWidth, y: (canvasWidth * a) + b};
406
407      if (point2.x < point1.x) {
408        [point1, point2] = [point2, point1];
409      }
410    }
411
412    context.beginPath();
413    context.moveTo(edgePoint1.x, edgePoint1.y);
414    context.lineTo(point1.x, point1.y);
415    context.moveTo(point2.x, point2.y);
416    context.lineTo(edgePoint2.x, edgePoint2.y);
417    context.stroke();
418  }
419
420  context.restore();
421}
422
423/**
424 * Draw all of the named grid area paths. This does not draw the labels, as
425 * placing labels in and around the grid for various things is handled later.
426 */
427function _drawGridAreas(
428    context: CanvasRenderingContext2D, areas: {[key: string]: Array<string|number>}, borderColor: string|undefined,
429    emulationScaleFactor: number): AreaBounds[] {
430  if (!areas || !Object.keys(areas).length) {
431    return [];
432  }
433
434  context.save();
435  if (borderColor) {
436    context.strokeStyle = borderColor;
437  }
438  context.lineWidth = 2;
439
440  const areaBounds = [];
441
442  for (const name in areas) {
443    const areaCommands = areas[name];
444
445    const bounds = emptyBounds();
446    const path = buildPath(areaCommands, bounds, emulationScaleFactor);
447
448    context.stroke(path);
449
450    areaBounds.push({name, bounds});
451  }
452
453  context.restore();
454
455  return areaBounds;
456}
457
458function _drawGridGap(
459    context: CanvasRenderingContext2D, gapCommands: Array<number|string>, gapColor: string|undefined,
460    hatchColor: string|undefined, rotationAngle: number, emulationScaleFactor: number,
461    flipDirection: boolean|undefined) {
462  if (!gapColor && !hatchColor) {
463    return;
464  }
465
466  context.save();
467  context.translate(0.5, 0.5);
468  context.lineWidth = 0;
469
470  const bounds = emptyBounds();
471  const path = buildPath(gapCommands, bounds, emulationScaleFactor);
472
473  // Fill the gap background if needed.
474  if (gapColor) {
475    context.fillStyle = gapColor;
476    context.fill(path);
477  }
478
479  // And draw the hatch pattern if needed.
480  if (hatchColor) {
481    _hatchFillPath(context, path, bounds, /* delta */ 10, hatchColor, rotationAngle, flipDirection);
482  }
483  context.restore();
484}
485
486/**
487 * Draw line hatching at a 45 degree angle for a given
488 * path.
489 *   __________
490 *   |\  \  \ |
491 *   | \  \  \|
492 *   |  \  \  |
493 *   |\  \  \ |
494 *   **********
495 */
496function _hatchFillPath(
497    context: CanvasRenderingContext2D, path: Path2D, bounds: Bounds, delta: number, color: string,
498    rotationAngle: number, flipDirection: boolean|undefined) {
499  const dx = bounds.maxX - bounds.minX;
500  const dy = bounds.maxY - bounds.minY;
501  context.rect(bounds.minX, bounds.minY, dx, dy);
502  context.save();
503  context.clip(path);
504  context.setLineDash([5, 3]);
505  const majorAxis = Math.max(dx, dy);
506  context.strokeStyle = color;
507  const centerX = bounds.minX + dx / 2;
508  const centerY = bounds.minY + dy / 2;
509  context.translate(centerX, centerY);
510  context.rotate(rotationAngle * Math.PI / 180);
511  context.translate(-centerX, -centerY);
512  if (flipDirection) {
513    for (let i = -majorAxis; i < majorAxis; i += delta) {
514      context.beginPath();
515      context.moveTo(bounds.maxX - i, bounds.minY);
516      context.lineTo(bounds.maxX - dy - i, bounds.maxY);
517      context.stroke();
518    }
519  } else {
520    for (let i = -majorAxis; i < majorAxis; i += delta) {
521      context.beginPath();
522      context.moveTo(i + bounds.minX, bounds.minY);
523      context.lineTo(dy + i + bounds.minX, bounds.maxY);
524      context.stroke();
525    }
526  }
527  context.restore();
528}
529