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 {isString} from '../base/object_utils'; 16import {globals} from '../frontend/globals'; 17 18export function cropText(str: string, charWidth: number, rectWidth: number) { 19 let displayText = ''; 20 const maxLength = Math.floor(rectWidth / charWidth) - 1; 21 if (str.length <= maxLength) { 22 displayText = str; 23 } else { 24 let limit = maxLength; 25 let maybeTripleDot = ''; 26 if (maxLength > 1) { 27 limit = maxLength - 1; 28 maybeTripleDot = '\u2026'; 29 } 30 // Javascript strings are UTF-16. |limit| could point in the middle of a 31 // 32-bit double-wchar codepoint (e.g., an emoji). Here we detect if the 32 // |limit|-th wchar is a leading surrogate and attach the trailing one. 33 const lastCharCode = str.charCodeAt(limit - 1); 34 limit += lastCharCode >= 0xd800 && lastCharCode < 0xdc00 ? 1 : 0; 35 displayText = str.substring(0, limit) + maybeTripleDot; 36 } 37 return displayText; 38} 39 40export function drawDoubleHeadedArrow( 41 ctx: CanvasRenderingContext2D, 42 x: number, 43 y: number, 44 length: number, 45 showArrowHeads: boolean, 46 width = 2, 47 color = 'black', 48) { 49 ctx.beginPath(); 50 ctx.lineWidth = width; 51 ctx.lineCap = 'round'; 52 ctx.strokeStyle = color; 53 ctx.moveTo(x, y); 54 ctx.lineTo(x + length, y); 55 ctx.stroke(); 56 ctx.closePath(); 57 // Arrowheads on the each end of the line. 58 if (showArrowHeads) { 59 ctx.beginPath(); 60 ctx.moveTo(x + length - 8, y - 4); 61 ctx.lineTo(x + length, y); 62 ctx.lineTo(x + length - 8, y + 4); 63 ctx.stroke(); 64 ctx.closePath(); 65 ctx.beginPath(); 66 ctx.moveTo(x + 8, y - 4); 67 ctx.lineTo(x, y); 68 ctx.lineTo(x + 8, y + 4); 69 ctx.stroke(); 70 ctx.closePath(); 71 } 72} 73 74export function drawIncompleteSlice( 75 ctx: CanvasRenderingContext2D, 76 x: number, 77 y: number, 78 width: number, 79 height: number, 80 showGradient: boolean = true, 81) { 82 if (width <= 0 || height <= 0) { 83 return; 84 } 85 ctx.beginPath(); 86 const triangleSize = height / 4; 87 ctx.moveTo(x, y); 88 ctx.lineTo(x + width, y); 89 ctx.lineTo(x + width - 3, y + triangleSize * 0.5); 90 ctx.lineTo(x + width, y + triangleSize); 91 ctx.lineTo(x + width - 3, y + triangleSize * 1.5); 92 ctx.lineTo(x + width, y + 2 * triangleSize); 93 ctx.lineTo(x + width - 3, y + triangleSize * 2.5); 94 ctx.lineTo(x + width, y + 3 * triangleSize); 95 ctx.lineTo(x + width - 3, y + triangleSize * 3.5); 96 ctx.lineTo(x + width, y + 4 * triangleSize); 97 ctx.lineTo(x, y + height); 98 99 const fillStyle = ctx.fillStyle; 100 if (isString(fillStyle)) { 101 if (showGradient) { 102 const gradient = ctx.createLinearGradient(x, y, x + width, y + height); 103 gradient.addColorStop(0.66, fillStyle); 104 gradient.addColorStop(1, '#FFFFFF'); 105 ctx.fillStyle = gradient; 106 } 107 } else { 108 throw new Error( 109 `drawIncompleteSlice() expects fillStyle to be a simple color not ${fillStyle}`, 110 ); 111 } 112 113 ctx.fill(); 114 ctx.fillStyle = fillStyle; 115} 116 117export function drawTrackHoverTooltip( 118 ctx: CanvasRenderingContext2D, 119 pos: {x: number; y: number}, 120 maxHeight: number, 121 text: string, 122 text2?: string, 123) { 124 ctx.font = '10px Roboto Condensed'; 125 ctx.textBaseline = 'middle'; 126 ctx.textAlign = 'left'; 127 128 // TODO(hjd): Avoid measuring text all the time (just use monospace?) 129 const textMetrics = ctx.measureText(text); 130 const text2Metrics = ctx.measureText(text2 || ''); 131 132 // Padding on each side of the box containing the tooltip: 133 const paddingPx = 4; 134 135 // Figure out the width of the tool tip box: 136 let width = Math.max(textMetrics.width, text2Metrics.width); 137 width += paddingPx * 2; 138 139 // and the height: 140 let height = 0; 141 height += textMetrics.fontBoundingBoxAscent; 142 height += textMetrics.fontBoundingBoxDescent; 143 if (text2 !== undefined) { 144 height += text2Metrics.fontBoundingBoxAscent; 145 height += text2Metrics.fontBoundingBoxDescent; 146 } 147 height += paddingPx * 2; 148 149 let x = pos.x; 150 let y = pos.y; 151 152 // Move box to the top right of the mouse: 153 x += 10; 154 y -= 10; 155 156 // Ensure the box is on screen: 157 const endPx = globals.timeline.visibleTimeScale.pxSpan.end; 158 if (x + width > endPx) { 159 x -= x + width - endPx; 160 } 161 if (y < 0) { 162 y = 0; 163 } 164 if (y + height > maxHeight) { 165 y -= y + height - maxHeight; 166 } 167 168 // Draw everything: 169 ctx.fillStyle = 'rgba(255, 255, 255, 0.9)'; 170 ctx.fillRect(x, y, width, height); 171 172 ctx.fillStyle = 'hsl(200, 50%, 40%)'; 173 ctx.fillText( 174 text, 175 x + paddingPx, 176 y + paddingPx + textMetrics.fontBoundingBoxAscent, 177 ); 178 if (text2 !== undefined) { 179 const yOffsetPx = 180 textMetrics.fontBoundingBoxAscent + 181 textMetrics.fontBoundingBoxDescent + 182 text2Metrics.fontBoundingBoxAscent; 183 ctx.fillText(text2, x + paddingPx, y + paddingPx + yOffsetPx); 184 } 185} 186 187export function canvasClip( 188 ctx: CanvasRenderingContext2D, 189 x: number, 190 y: number, 191 w: number, 192 h: number, 193): void { 194 ctx.beginPath(); 195 ctx.rect(x, y, w, h); 196 ctx.clip(); 197} 198