xref: /aosp_15_r20/external/perfetto/ui/src/base/canvas_utils.ts (revision 6dbdd20afdafa5e3ca9b8809fa73465d530080dc)
1// Copyright (C) 2019 The Android Open Source Project
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//      http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15import {Size2D, Point2D} from './geom';
16import {isString} from './object_utils';
17
18export function drawDoubleHeadedArrow(
19  ctx: CanvasRenderingContext2D,
20  x: number,
21  y: number,
22  length: number,
23  showArrowHeads: boolean,
24  width = 2,
25  color = 'black',
26) {
27  ctx.beginPath();
28  ctx.lineWidth = width;
29  ctx.lineCap = 'round';
30  ctx.strokeStyle = color;
31  ctx.moveTo(x, y);
32  ctx.lineTo(x + length, y);
33  ctx.stroke();
34  ctx.closePath();
35  // Arrowheads on the each end of the line.
36  if (showArrowHeads) {
37    ctx.beginPath();
38    ctx.moveTo(x + length - 8, y - 4);
39    ctx.lineTo(x + length, y);
40    ctx.lineTo(x + length - 8, y + 4);
41    ctx.stroke();
42    ctx.closePath();
43    ctx.beginPath();
44    ctx.moveTo(x + 8, y - 4);
45    ctx.lineTo(x, y);
46    ctx.lineTo(x + 8, y + 4);
47    ctx.stroke();
48    ctx.closePath();
49  }
50}
51
52export function drawIncompleteSlice(
53  ctx: CanvasRenderingContext2D,
54  x: number,
55  y: number,
56  width: number,
57  height: number,
58  showGradient: boolean = true,
59) {
60  if (width <= 0 || height <= 0) {
61    return;
62  }
63  ctx.beginPath();
64  const triangleSize = height / 4;
65  ctx.moveTo(x, y);
66  ctx.lineTo(x + width, y);
67  ctx.lineTo(x + width - 3, y + triangleSize * 0.5);
68  ctx.lineTo(x + width, y + triangleSize);
69  ctx.lineTo(x + width - 3, y + triangleSize * 1.5);
70  ctx.lineTo(x + width, y + 2 * triangleSize);
71  ctx.lineTo(x + width - 3, y + triangleSize * 2.5);
72  ctx.lineTo(x + width, y + 3 * triangleSize);
73  ctx.lineTo(x + width - 3, y + triangleSize * 3.5);
74  ctx.lineTo(x + width, y + 4 * triangleSize);
75  ctx.lineTo(x, y + height);
76
77  const fillStyle = ctx.fillStyle;
78  if (isString(fillStyle)) {
79    if (showGradient) {
80      const gradient = ctx.createLinearGradient(x, y, x + width, y + height);
81      gradient.addColorStop(0.66, fillStyle);
82      gradient.addColorStop(1, '#FFFFFF');
83      ctx.fillStyle = gradient;
84    }
85  } else {
86    throw new Error(
87      `drawIncompleteSlice() expects fillStyle to be a simple color not ${fillStyle}`,
88    );
89  }
90
91  ctx.fill();
92  ctx.fillStyle = fillStyle;
93}
94
95export function drawTrackHoverTooltip(
96  ctx: CanvasRenderingContext2D,
97  pos: Point2D,
98  trackSize: Size2D,
99  text: string,
100  text2?: string,
101) {
102  ctx.font = '10px Roboto Condensed';
103  ctx.textBaseline = 'middle';
104  ctx.textAlign = 'left';
105
106  // TODO(hjd): Avoid measuring text all the time (just use monospace?)
107  const textMetrics = ctx.measureText(text);
108  const text2Metrics = ctx.measureText(text2 ?? '');
109
110  // Padding on each side of the box containing the tooltip:
111  const paddingPx = 4;
112
113  // Figure out the width of the tool tip box:
114  let width = Math.max(textMetrics.width, text2Metrics.width);
115  width += paddingPx * 2;
116
117  // and the height:
118  let height = 0;
119  height += textMetrics.fontBoundingBoxAscent;
120  height += textMetrics.fontBoundingBoxDescent;
121  if (text2 !== undefined) {
122    height += text2Metrics.fontBoundingBoxAscent;
123    height += text2Metrics.fontBoundingBoxDescent;
124  }
125  height += paddingPx * 2;
126
127  let x = pos.x;
128  let y = pos.y;
129
130  // Move box to the top right of the mouse:
131  x += 10;
132  y -= 10;
133
134  // Ensure the box is on screen:
135  const endPx = trackSize.width;
136  if (x + width > endPx) {
137    x -= x + width - endPx;
138  }
139  if (y < 0) {
140    y = 0;
141  }
142  if (y + height > trackSize.height) {
143    y -= y + height - trackSize.height;
144  }
145
146  // Draw everything:
147  ctx.fillStyle = 'rgba(255, 255, 255, 0.9)';
148  ctx.fillRect(x, y, width, height);
149
150  ctx.fillStyle = 'hsl(200, 50%, 40%)';
151  ctx.fillText(
152    text,
153    x + paddingPx,
154    y + paddingPx + textMetrics.fontBoundingBoxAscent,
155  );
156  if (text2 !== undefined) {
157    const yOffsetPx =
158      textMetrics.fontBoundingBoxAscent +
159      textMetrics.fontBoundingBoxDescent +
160      text2Metrics.fontBoundingBoxAscent;
161    ctx.fillText(text2, x + paddingPx, y + paddingPx + yOffsetPx);
162  }
163}
164
165export function canvasClip(
166  ctx: CanvasRenderingContext2D,
167  x: number,
168  y: number,
169  w: number,
170  h: number,
171): void {
172  ctx.beginPath();
173  ctx.rect(x, y, w, h);
174  ctx.clip();
175}
176
177/**
178 * Save the state of the canvas, returning a disposable which restores the state
179 * when disposed.
180 *
181 * Allows using the |using| keyword to automatically restore the canvas state.
182 * @param ctx - The canvas context to save the state of.
183 * @returns A disposable.
184 *
185 * @example
186 * {
187 *   using const _ = canvasSave(ctx);
188 *   ctx.translate(123, 456); // Manipulate the canvas state
189 * } // ctx.restore() is automatically called when the _ falls out of scope
190 */
191export function canvasSave(ctx: CanvasRenderingContext2D): Disposable {
192  ctx.save();
193  return {
194    [Symbol.dispose](): void {
195      ctx.restore();
196    },
197  };
198}
199