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 size 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 m from 'mithril'; 16import {BigintMath} from '../base/bigint_math'; 17 18import {Span, tpTimeToString} from '../common/time'; 19import { 20 TPTime, 21 TPTimeSpan, 22} from '../common/time'; 23 24import { 25 BACKGROUND_COLOR, 26 FOREGROUND_COLOR, 27 TRACK_SHELL_WIDTH, 28} from './css_constants'; 29import {globals} from './globals'; 30import { 31 getMaxMajorTicks, 32 TickGenerator, 33 TickType, 34 timeScaleForVisibleWindow, 35} from './gridline_helper'; 36import {Panel, PanelSize} from './panel'; 37 38export interface BBox { 39 x: number; 40 y: number; 41 width: number; 42 height: number; 43} 44 45// Draws a vertical line with two horizontal tails at the left and right and 46// a label in the middle. It looks a bit like a stretched H: 47// |--- Label ---| 48// The |target| bounding box determines where to draw the H. 49// The |bounds| bounding box gives the visible region, this is used to adjust 50// the positioning of the label to ensure it is on screen. 51function drawHBar( 52 ctx: CanvasRenderingContext2D, target: BBox, bounds: BBox, label: string) { 53 ctx.fillStyle = FOREGROUND_COLOR; 54 55 const xLeft = Math.floor(target.x); 56 const xRight = Math.floor(target.x + target.width); 57 const yMid = Math.floor(target.height / 2 + target.y); 58 const xWidth = xRight - xLeft; 59 60 // Don't draw in the track shell. 61 ctx.beginPath(); 62 ctx.rect(bounds.x, bounds.y, bounds.width, bounds.height); 63 ctx.clip(); 64 65 // Draw horizontal bar of the H. 66 ctx.fillRect(xLeft, yMid, xWidth, 1); 67 // Draw left vertical bar of the H. 68 ctx.fillRect(xLeft, target.y, 1, target.height); 69 // Draw right vertical bar of the H. 70 ctx.fillRect(xRight, target.y, 1, target.height); 71 72 const labelWidth = ctx.measureText(label).width; 73 74 // Find a good position for the label: 75 // By default put the label in the middle of the H: 76 let labelXLeft = Math.floor(xWidth / 2 - labelWidth / 2 + xLeft); 77 78 if (labelWidth > target.width || labelXLeft < bounds.x || 79 (labelXLeft + labelWidth) > (bounds.x + bounds.width)) { 80 // It won't fit in the middle or would be at least partly out of bounds 81 // so put it either to the left or right: 82 if (xRight > bounds.x + bounds.width) { 83 // If the H extends off the right side of the screen the label 84 // goes on the left of the H. 85 labelXLeft = xLeft - labelWidth - 3; 86 } else { 87 // Otherwise the label goes on the right of the H. 88 labelXLeft = xRight + 3; 89 } 90 } 91 92 ctx.fillStyle = BACKGROUND_COLOR; 93 ctx.fillRect(labelXLeft - 1, 0, labelWidth + 1, target.height); 94 95 ctx.textBaseline = 'middle'; 96 ctx.fillStyle = FOREGROUND_COLOR; 97 ctx.font = '10px Roboto Condensed'; 98 ctx.fillText(label, labelXLeft, yMid); 99} 100 101function drawIBar( 102 ctx: CanvasRenderingContext2D, xPos: number, bounds: BBox, label: string) { 103 if (xPos < bounds.x) return; 104 105 ctx.fillStyle = FOREGROUND_COLOR; 106 ctx.fillRect(xPos, 0, 1, bounds.width); 107 108 const yMid = Math.floor(bounds.height / 2 + bounds.y); 109 const labelWidth = ctx.measureText(label).width; 110 const padding = 3; 111 112 let xPosLabel; 113 if (xPos + padding + labelWidth > bounds.width) { 114 xPosLabel = xPos - padding; 115 ctx.textAlign = 'right'; 116 } else { 117 xPosLabel = xPos + padding; 118 ctx.textAlign = 'left'; 119 } 120 121 ctx.fillStyle = BACKGROUND_COLOR; 122 ctx.fillRect(xPosLabel - 1, 0, labelWidth + 2, bounds.height); 123 124 ctx.textBaseline = 'middle'; 125 ctx.fillStyle = FOREGROUND_COLOR; 126 ctx.font = '10px Roboto Condensed'; 127 ctx.fillText(label, xPosLabel, yMid); 128} 129 130export class TimeSelectionPanel extends Panel { 131 view() { 132 return m('.time-selection-panel'); 133 } 134 135 renderCanvas(ctx: CanvasRenderingContext2D, size: PanelSize) { 136 ctx.fillStyle = '#999'; 137 ctx.fillRect(TRACK_SHELL_WIDTH - 2, 0, 2, size.height); 138 139 ctx.save(); 140 ctx.beginPath(); 141 ctx.rect(TRACK_SHELL_WIDTH, 0, size.width - TRACK_SHELL_WIDTH, size.height); 142 ctx.clip(); 143 144 const span = globals.frontendLocalState.visibleWindow.timestampSpan; 145 if (size.width > TRACK_SHELL_WIDTH && span.duration > 0n) { 146 const maxMajorTicks = getMaxMajorTicks(size.width - TRACK_SHELL_WIDTH); 147 const map = timeScaleForVisibleWindow(TRACK_SHELL_WIDTH, size.width); 148 for (const {type, time} of new TickGenerator( 149 span, maxMajorTicks, globals.state.traceTime.start)) { 150 const px = Math.floor(map.tpTimeToPx(time)); 151 if (type === TickType.MAJOR) { 152 ctx.fillRect(px, 0, 1, size.height); 153 } 154 } 155 } 156 157 const localArea = globals.frontendLocalState.selectedArea; 158 const selection = globals.state.currentSelection; 159 if (localArea !== undefined) { 160 const start = BigintMath.min(localArea.start, localArea.end); 161 const end = BigintMath.max(localArea.start, localArea.end); 162 this.renderSpan(ctx, size, new TPTimeSpan(start, end)); 163 } else if (selection !== null && selection.kind === 'AREA') { 164 const selectedArea = globals.state.areas[selection.areaId]; 165 const start = BigintMath.min(selectedArea.start, selectedArea.end); 166 const end = BigintMath.max(selectedArea.start, selectedArea.end); 167 this.renderSpan(ctx, size, new TPTimeSpan(start, end)); 168 } 169 170 if (globals.state.hoverCursorTimestamp !== -1n) { 171 this.renderHover(ctx, size, globals.state.hoverCursorTimestamp); 172 } 173 174 for (const note of Object.values(globals.state.notes)) { 175 const noteIsSelected = selection !== null && selection.kind === 'AREA' && 176 selection.noteId === note.id; 177 if (note.noteType === 'AREA' && !noteIsSelected) { 178 const selectedArea = globals.state.areas[note.areaId]; 179 this.renderSpan( 180 ctx, size, new TPTimeSpan(selectedArea.start, selectedArea.end)); 181 } 182 } 183 184 ctx.restore(); 185 } 186 187 renderHover(ctx: CanvasRenderingContext2D, size: PanelSize, ts: TPTime) { 188 const {visibleTimeScale} = globals.frontendLocalState; 189 const xPos = 190 TRACK_SHELL_WIDTH + Math.floor(visibleTimeScale.tpTimeToPx(ts)); 191 const offsetTime = tpTimeToString(ts - globals.state.traceTime.start); 192 const timeFromStart = tpTimeToString(ts); 193 const label = `${offsetTime} (${timeFromStart})`; 194 drawIBar(ctx, xPos, this.bounds(size), label); 195 } 196 197 renderSpan( 198 ctx: CanvasRenderingContext2D, size: PanelSize, span: Span<TPTime>) { 199 const {visibleTimeScale} = globals.frontendLocalState; 200 const xLeft = visibleTimeScale.tpTimeToPx(span.start); 201 const xRight = visibleTimeScale.tpTimeToPx(span.end); 202 const label = tpTimeToString(span.duration); 203 drawHBar( 204 ctx, 205 { 206 x: TRACK_SHELL_WIDTH + xLeft, 207 y: 0, 208 width: xRight - xLeft, 209 height: size.height, 210 }, 211 this.bounds(size), 212 label); 213 } 214 215 private bounds(size: PanelSize): BBox { 216 return { 217 x: TRACK_SHELL_WIDTH, 218 y: 0, 219 width: size.width - TRACK_SHELL_WIDTH, 220 height: size.height, 221 }; 222 } 223} 224