UNPKG

15.6 kBPlain TextView Raw
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
31import type {AreaBounds, Bounds} from './common.js';
32import type {GridLabelState} from './css_grid_label_helpers.js';
33import {drawGridLabels} from './css_grid_label_helpers.js';
34import {applyMatrixToPoint, buildPath, emptyBounds, hatchFillPath} from './highlight_common.js';
35
36// TODO(alexrudenko): Grid label unit tests depend on this style so it cannot be extracted yet.
37export const gridStyle = `
38/* Grid row and column labels */
39.grid-label-content {
40 position: absolute;
41 -webkit-user-select: none;
42 padding: 2px;
43 font-family: Menlo, monospace;
44 font-size: 10px;
45 min-width: 17px;
46 min-height: 15px;
47 border-radius: 2px;
48 box-sizing: border-box;
49 z-index: 1;
50 background-clip: padding-box;
51 pointer-events: none;
52 text-align: center;
53 display: flex;
54 justify-content: center;
55 align-items: center;
56}
57
58.grid-label-content[data-direction=row] {
59 background-color: var(--row-label-color, #1A73E8);
60 color: var(--row-label-text-color, #121212);
61}
62
63.grid-label-content[data-direction=column] {
64 background-color: var(--column-label-color, #1A73E8);
65 color: var(--column-label-text-color,#121212);
66}
67
68.line-names ul,
69.line-names .line-name {
70 margin: 0;
71 padding: 0;
72 list-style: none;
73}
74
75.line-names .line-name {
76 max-width: 100px;
77 white-space: nowrap;
78 overflow: hidden;
79 text-overflow: ellipsis;
80}
81
82.line-names .grid-label-content,
83.line-numbers .grid-label-content,
84.track-sizes .grid-label-content {
85 border: 1px solid white;
86 --inner-corner-avoid-distance: 15px;
87}
88
89.grid-label-content.top-left.inner-shared-corner,
90.grid-label-content.top-right.inner-shared-corner {
91 transform: translateY(var(--inner-corner-avoid-distance));
92}
93
94.grid-label-content.bottom-left.inner-shared-corner,
95.grid-label-content.bottom-right.inner-shared-corner {
96 transform: translateY(calc(var(--inner-corner-avoid-distance) * -1));
97}
98
99.grid-label-content.left-top.inner-shared-corner,
100.grid-label-content.left-bottom.inner-shared-corner {
101 transform: translateX(var(--inner-corner-avoid-distance));
102}
103
104.grid-label-content.right-top.inner-shared-corner,
105.grid-label-content.right-bottom.inner-shared-corner {
106 transform: translateX(calc(var(--inner-corner-avoid-distance) * -1));
107}
108
109.line-names .grid-label-content::before,
110.line-numbers .grid-label-content::before,
111.track-sizes .grid-label-content::before {
112 position: absolute;
113 z-index: 1;
114 pointer-events: none;
115 content: "";
116 width: 3px;
117 height: 3px;
118 border: 1px solid white;
119 border-width: 0 1px 1px 0;
120}
121
122.line-names .grid-label-content[data-direction=row]::before,
123.line-numbers .grid-label-content[data-direction=row]::before,
124.track-sizes .grid-label-content[data-direction=row]::before {
125 background: var(--row-label-color, #1A73E8);
126}
127
128.line-names .grid-label-content[data-direction=column]::before,
129.line-numbers .grid-label-content[data-direction=column]::before,
130.track-sizes .grid-label-content[data-direction=column]::before {
131 background: var(--column-label-color, #1A73E8);
132}
133
134.grid-label-content.bottom-mid::before {
135 transform: translateY(-1px) rotate(45deg);
136 top: 100%;
137}
138
139.grid-label-content.top-mid::before {
140 transform: translateY(-3px) rotate(-135deg);
141 top: 0%;
142}
143
144.grid-label-content.left-mid::before {
145 transform: translateX(-3px) rotate(135deg);
146 left: 0%
147}
148
149.grid-label-content.right-mid::before {
150 transform: translateX(3px) rotate(-45deg);
151 right: 0%;
152}
153
154.grid-label-content.right-top::before {
155 transform: translateX(3px) translateY(-1px) rotate(-90deg) skewY(30deg);
156 right: 0%;
157 top: 0%;
158}
159
160.grid-label-content.right-bottom::before {
161 transform: translateX(3px) translateY(-3px) skewX(30deg);
162 right: 0%;
163 top: 100%;
164}
165
166.grid-label-content.bottom-right::before {
167 transform: translateX(1px) translateY(-1px) skewY(30deg);
168 right: 0%;
169 top: 100%;
170}
171
172.grid-label-content.bottom-left::before {
173 transform: translateX(-1px) translateY(-1px) rotate(90deg) skewX(30deg);
174 left: 0%;
175 top: 100%;
176}
177
178.grid-label-content.left-top::before {
179 transform: translateX(-3px) translateY(-1px) rotate(180deg) skewX(30deg);
180 left: 0%;
181 top: 0%;
182}
183
184.grid-label-content.left-bottom::before {
185 transform: translateX(-3px) translateY(-3px) rotate(90deg) skewY(30deg);
186 left: 0%;
187 top: 100%;
188}
189
190.grid-label-content.top-right::before {
191 transform: translateX(1px) translateY(-3px) rotate(-90deg) skewX(30deg);
192 right: 0%;
193 top: 0%;
194}
195
196.grid-label-content.top-left::before {
197 transform: translateX(-1px) translateY(-3px) rotate(180deg) skewY(30deg);
198 left: 0%;
199 top: 0%;
200}
201
202@media (forced-colors: active) {
203 .grid-label-content {
204 border-color: Highlight;
205 background-color: Canvas;
206 color: Text;
207 forced-color-adjust: none;
208 }
209 .grid-label-content::before {
210 background-color: Canvas;
211 border-color: Highlight;
212 }
213}`;
214
215export interface GridHighlight {
216 gridBorder: Array<string|number>;
217 writingMode: string;
218 rowGaps: Array<string|number>;
219 rotationAngle: number;
220 columnGaps: Array<string|number>;
221 rows: Array<string|number>;
222 columns: Array<string|number>;
223 areaNames: {[key: string]: Array<string|number>};
224 gridHighlightConfig: {
225 gridBackgroundColor?: string,
226 gridBorderColor?: string,
227 rowGapColor?: string,
228 columnGapColor?: string,
229 areaBorderColor?: string,
230 gridBorderDash: boolean,
231 rowLineDash: boolean,
232 columnLineDash: boolean,
233 showGridExtensionLines: boolean,
234 showPositiveLineNumbers: boolean,
235 showNegativeLineNumbers: boolean,
236 rowLineColor: string,
237 columnLineColor: string,
238 rowHatchColor: string,
239 columnHatchColor: string,
240 showLineNames: boolean,
241 };
242}
243
244export function drawLayoutGridHighlight(
245 highlight: GridHighlight, context: CanvasRenderingContext2D, deviceScaleFactor: number, canvasWidth: number,
246 canvasHeight: number, emulationScaleFactor: number, labelState: GridLabelState) {
247 const gridBounds = emptyBounds();
248 const gridPath = buildPath(highlight.gridBorder, gridBounds, emulationScaleFactor);
249
250 // Transform the context to match the current writing-mode.
251 context.save();
252 applyWritingModeTransformation(highlight.writingMode, gridBounds, context);
253
254 // Draw grid background
255 if (highlight.gridHighlightConfig.gridBackgroundColor) {
256 context.fillStyle = highlight.gridHighlightConfig.gridBackgroundColor;
257 context.fill(gridPath);
258 }
259
260 // Draw Grid border
261 if (highlight.gridHighlightConfig.gridBorderColor) {
262 context.save();
263 context.translate(0.5, 0.5);
264 context.lineWidth = 0;
265 if (highlight.gridHighlightConfig.gridBorderDash) {
266 context.setLineDash([3, 3]);
267 }
268 context.strokeStyle = highlight.gridHighlightConfig.gridBorderColor;
269 context.stroke(gridPath);
270 context.restore();
271 }
272
273 // Draw grid lines
274 const rowBounds = drawGridLines(context, highlight, 'row', emulationScaleFactor);
275 const columnBounds = drawGridLines(context, highlight, 'column', emulationScaleFactor);
276
277 // Draw gaps
278 drawGridGap(
279 context, highlight.rowGaps, highlight.gridHighlightConfig.rowGapColor,
280 highlight.gridHighlightConfig.rowHatchColor, highlight.rotationAngle, emulationScaleFactor,
281 /* flipDirection */ true);
282 drawGridGap(
283 context, highlight.columnGaps, highlight.gridHighlightConfig.columnGapColor,
284 highlight.gridHighlightConfig.columnHatchColor, highlight.rotationAngle, emulationScaleFactor,
285 /* flipDirection */ false);
286
287 // Draw named grid areas
288 const areaBounds =
289 drawGridAreas(context, highlight.areaNames, highlight.gridHighlightConfig.areaBorderColor, emulationScaleFactor);
290
291 // The rest of the overlay is drawn without the writing-mode transformation, but we keep the matrix to transform relevant points.
292 const writingModeMatrix = context.getTransform();
293 writingModeMatrix.scaleSelf(1 / deviceScaleFactor);
294 context.restore();
295
296 if (highlight.gridHighlightConfig.showGridExtensionLines) {
297 if (rowBounds) {
298 drawExtendedGridLines(
299 context, rowBounds, highlight.gridHighlightConfig.rowLineColor, highlight.gridHighlightConfig.rowLineDash,
300 writingModeMatrix, canvasWidth, canvasHeight);
301 }
302 if (columnBounds) {
303 drawExtendedGridLines(
304 context, columnBounds, highlight.gridHighlightConfig.columnLineColor,
305 highlight.gridHighlightConfig.columnLineDash, writingModeMatrix, canvasWidth, canvasHeight);
306 }
307 }
308
309 // Draw all the labels
310 drawGridLabels(
311 highlight, gridBounds, areaBounds, {canvasWidth, canvasHeight}, labelState, emulationScaleFactor,
312 writingModeMatrix);
313}
314
315function applyWritingModeTransformation(writingMode: string, gridBounds: Bounds, context: CanvasRenderingContext2D) {
316 if (writingMode !== 'vertical-rl' && writingMode !== 'vertical-lr') {
317 return;
318 }
319
320 const topLeft = gridBounds.allPoints[0];
321 const bottomLeft = gridBounds.allPoints[3];
322
323 // Move to the top-left corner to do all transformations there.
324 context.translate(topLeft.x, topLeft.y);
325
326 if (writingMode === 'vertical-rl') {
327 context.rotate(90 * Math.PI / 180);
328 context.translate(0, -1 * (bottomLeft.y - topLeft.y));
329 }
330
331 if (writingMode === 'vertical-lr') {
332 context.rotate(90 * Math.PI / 180);
333 context.scale(1, -1);
334 }
335
336 // Move back to the original point.
337 context.translate(topLeft.x * -1, topLeft.y * -1);
338}
339
340function drawGridLines(
341 context: CanvasRenderingContext2D, highlight: GridHighlight, direction: 'row'|'column',
342 emulationScaleFactor: number) {
343 const tracks = highlight[`${direction}s` as 'rows' | 'columns'];
344 const color = highlight.gridHighlightConfig[`${direction}LineColor` as 'rowLineColor' | 'columnLineColor'];
345 const dash = highlight.gridHighlightConfig[`${direction}LineDash` as 'rowLineDash' | 'columnLineDash'];
346
347 if (!color) {
348 return null;
349 }
350
351 const bounds = emptyBounds();
352 const path = buildPath(tracks, bounds, emulationScaleFactor);
353
354 context.save();
355 context.translate(0.5, 0.5);
356 if (dash) {
357 context.setLineDash([3, 3]);
358 }
359 context.lineWidth = 0;
360 context.strokeStyle = color;
361
362 context.save();
363 context.stroke(path);
364 context.restore();
365
366 context.restore();
367
368 return bounds;
369}
370
371function drawExtendedGridLines(
372 context: CanvasRenderingContext2D, bounds: Bounds, color: string, dash: boolean|undefined,
373 writingModeMatrix: DOMMatrix, canvasWidth: number, canvasHeight: number) {
374 context.save();
375 context.strokeStyle = color;
376 context.lineWidth = 1;
377 context.translate(0.5, 0.5);
378 if (dash) {
379 context.setLineDash([3, 3]);
380 }
381
382 // A grid track path is a list of lines defined by 2 points.
383 // 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.
384 for (let i = 0; i < bounds.allPoints.length; i += 2) {
385 let point1 = applyMatrixToPoint(bounds.allPoints[i], writingModeMatrix);
386 let point2 = applyMatrixToPoint(bounds.allPoints[i + 1], writingModeMatrix);
387 let edgePoint1;
388 let edgePoint2;
389
390 if (point1.x === point2.x) {
391 // Special case for a vertical line.
392 edgePoint1 = {x: point1.x, y: 0};
393 edgePoint2 = {x: point1.x, y: canvasHeight};
394 if (point2.y < point1.y) {
395 [point1, point2] = [point2, point1];
396 }
397 } else if (point1.y === point2.y) {
398 // Special case for a horizontal line.
399 edgePoint1 = {x: 0, y: point1.y};
400 edgePoint2 = {x: canvasWidth, y: point1.y};
401 if (point2.x < point1.x) {
402 [point1, point2] = [point2, point1];
403 }
404 } else {
405 // When the line isn't straight, we need to do some maths.
406 const a = (point2.y - point1.y) / (point2.x - point1.x);
407 const b = (point1.y * point2.x - point2.y * point1.x) / (point2.x - point1.x);
408
409 edgePoint1 = {x: 0, y: b};
410 edgePoint2 = {x: canvasWidth, y: (canvasWidth * a) + b};
411
412 if (point2.x < point1.x) {
413 [point1, point2] = [point2, point1];
414 }
415 }
416
417 context.beginPath();
418 context.moveTo(edgePoint1.x, edgePoint1.y);
419 context.lineTo(point1.x, point1.y);
420 context.moveTo(point2.x, point2.y);
421 context.lineTo(edgePoint2.x, edgePoint2.y);
422 context.stroke();
423 }
424
425 context.restore();
426}
427
428/**
429 * Draw all of the named grid area paths. This does not draw the labels, as
430 * placing labels in and around the grid for various things is handled later.
431 */
432function drawGridAreas(
433 context: CanvasRenderingContext2D, areas: {[key: string]: Array<string|number>}, borderColor: string|undefined,
434 emulationScaleFactor: number): AreaBounds[] {
435 if (!areas || !Object.keys(areas).length) {
436 return [];
437 }
438
439 context.save();
440 if (borderColor) {
441 context.strokeStyle = borderColor;
442 }
443 context.lineWidth = 2;
444
445 const areaBounds = [];
446
447 for (const name in areas) {
448 const areaCommands = areas[name];
449
450 const bounds = emptyBounds();
451 const path = buildPath(areaCommands, bounds, emulationScaleFactor);
452
453 context.stroke(path);
454
455 areaBounds.push({name, bounds});
456 }
457
458 context.restore();
459
460 return areaBounds;
461}
462
463function drawGridGap(
464 context: CanvasRenderingContext2D, gapCommands: Array<number|string>, gapColor: string|undefined,
465 hatchColor: string|undefined, rotationAngle: number, emulationScaleFactor: number,
466 flipDirection: boolean|undefined) {
467 if (!gapColor && !hatchColor) {
468 return;
469 }
470
471 context.save();
472 context.translate(0.5, 0.5);
473 context.lineWidth = 0;
474
475 const bounds = emptyBounds();
476 const path = buildPath(gapCommands, bounds, emulationScaleFactor);
477
478 // Fill the gap background if needed.
479 if (gapColor) {
480 context.fillStyle = gapColor;
481 context.fill(path);
482 }
483
484 // And draw the hatch pattern if needed.
485 if (hatchColor) {
486 hatchFillPath(context, path, bounds, /* delta */ 10, hatchColor, rotationAngle, flipDirection);
487 }
488 context.restore();
489}