| // Copyright (C) 2019 The Android Open Source Project |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| import {isString} from '../base/object_utils'; |
| import {globals} from '../frontend/globals'; |
| |
| export function cropText(str: string, charWidth: number, rectWidth: number) { |
| let displayText = ''; |
| const maxLength = Math.floor(rectWidth / charWidth) - 1; |
| if (str.length <= maxLength) { |
| displayText = str; |
| } else { |
| let limit = maxLength; |
| let maybeTripleDot = ''; |
| if (maxLength > 1) { |
| limit = maxLength - 1; |
| maybeTripleDot = '\u2026'; |
| } |
| // Javascript strings are UTF-16. |limit| could point in the middle of a |
| // 32-bit double-wchar codepoint (e.g., an emoji). Here we detect if the |
| // |limit|-th wchar is a leading surrogate and attach the trailing one. |
| const lastCharCode = str.charCodeAt(limit - 1); |
| limit += lastCharCode >= 0xd800 && lastCharCode < 0xdc00 ? 1 : 0; |
| displayText = str.substring(0, limit) + maybeTripleDot; |
| } |
| return displayText; |
| } |
| |
| export function drawDoubleHeadedArrow( |
| ctx: CanvasRenderingContext2D, |
| x: number, |
| y: number, |
| length: number, |
| showArrowHeads: boolean, |
| width = 2, |
| color = 'black', |
| ) { |
| ctx.beginPath(); |
| ctx.lineWidth = width; |
| ctx.lineCap = 'round'; |
| ctx.strokeStyle = color; |
| ctx.moveTo(x, y); |
| ctx.lineTo(x + length, y); |
| ctx.stroke(); |
| ctx.closePath(); |
| // Arrowheads on the each end of the line. |
| if (showArrowHeads) { |
| ctx.beginPath(); |
| ctx.moveTo(x + length - 8, y - 4); |
| ctx.lineTo(x + length, y); |
| ctx.lineTo(x + length - 8, y + 4); |
| ctx.stroke(); |
| ctx.closePath(); |
| ctx.beginPath(); |
| ctx.moveTo(x + 8, y - 4); |
| ctx.lineTo(x, y); |
| ctx.lineTo(x + 8, y + 4); |
| ctx.stroke(); |
| ctx.closePath(); |
| } |
| } |
| |
| export function drawIncompleteSlice( |
| ctx: CanvasRenderingContext2D, |
| x: number, |
| y: number, |
| width: number, |
| height: number, |
| showGradient: boolean = true, |
| ) { |
| if (width <= 0 || height <= 0) { |
| return; |
| } |
| ctx.beginPath(); |
| const triangleSize = height / 4; |
| ctx.moveTo(x, y); |
| ctx.lineTo(x + width, y); |
| ctx.lineTo(x + width - 3, y + triangleSize * 0.5); |
| ctx.lineTo(x + width, y + triangleSize); |
| ctx.lineTo(x + width - 3, y + triangleSize * 1.5); |
| ctx.lineTo(x + width, y + 2 * triangleSize); |
| ctx.lineTo(x + width - 3, y + triangleSize * 2.5); |
| ctx.lineTo(x + width, y + 3 * triangleSize); |
| ctx.lineTo(x + width - 3, y + triangleSize * 3.5); |
| ctx.lineTo(x + width, y + 4 * triangleSize); |
| ctx.lineTo(x, y + height); |
| |
| const fillStyle = ctx.fillStyle; |
| if (isString(fillStyle)) { |
| if (showGradient) { |
| const gradient = ctx.createLinearGradient(x, y, x + width, y + height); |
| gradient.addColorStop(0.66, fillStyle); |
| gradient.addColorStop(1, '#FFFFFF'); |
| ctx.fillStyle = gradient; |
| } |
| } else { |
| throw new Error( |
| `drawIncompleteSlice() expects fillStyle to be a simple color not ${fillStyle}`, |
| ); |
| } |
| |
| ctx.fill(); |
| ctx.fillStyle = fillStyle; |
| } |
| |
| export function drawTrackHoverTooltip( |
| ctx: CanvasRenderingContext2D, |
| pos: {x: number; y: number}, |
| maxHeight: number, |
| text: string, |
| text2?: string, |
| ) { |
| ctx.font = '10px Roboto Condensed'; |
| ctx.textBaseline = 'middle'; |
| ctx.textAlign = 'left'; |
| |
| // TODO(hjd): Avoid measuring text all the time (just use monospace?) |
| const textMetrics = ctx.measureText(text); |
| const text2Metrics = ctx.measureText(text2 || ''); |
| |
| // Padding on each side of the box containing the tooltip: |
| const paddingPx = 4; |
| |
| // Figure out the width of the tool tip box: |
| let width = Math.max(textMetrics.width, text2Metrics.width); |
| width += paddingPx * 2; |
| |
| // and the height: |
| let height = 0; |
| height += textMetrics.fontBoundingBoxAscent; |
| height += textMetrics.fontBoundingBoxDescent; |
| if (text2 !== undefined) { |
| height += text2Metrics.fontBoundingBoxAscent; |
| height += text2Metrics.fontBoundingBoxDescent; |
| } |
| height += paddingPx * 2; |
| |
| let x = pos.x; |
| let y = pos.y; |
| |
| // Move box to the top right of the mouse: |
| x += 10; |
| y -= 10; |
| |
| // Ensure the box is on screen: |
| const endPx = globals.timeline.visibleTimeScale.pxSpan.end; |
| if (x + width > endPx) { |
| x -= x + width - endPx; |
| } |
| if (y < 0) { |
| y = 0; |
| } |
| if (y + height > maxHeight) { |
| y -= y + height - maxHeight; |
| } |
| |
| // Draw everything: |
| ctx.fillStyle = 'rgba(255, 255, 255, 0.9)'; |
| ctx.fillRect(x, y, width, height); |
| |
| ctx.fillStyle = 'hsl(200, 50%, 40%)'; |
| ctx.fillText( |
| text, |
| x + paddingPx, |
| y + paddingPx + textMetrics.fontBoundingBoxAscent, |
| ); |
| if (text2 !== undefined) { |
| const yOffsetPx = |
| textMetrics.fontBoundingBoxAscent + |
| textMetrics.fontBoundingBoxDescent + |
| text2Metrics.fontBoundingBoxAscent; |
| ctx.fillText(text2, x + paddingPx, y + paddingPx + yOffsetPx); |
| } |
| } |
| |
| export function canvasClip( |
| ctx: CanvasRenderingContext2D, |
| x: number, |
| y: number, |
| w: number, |
| h: number, |
| ): void { |
| ctx.beginPath(); |
| ctx.rect(x, y, w, h); |
| ctx.clip(); |
| } |