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'; 16 17import {duration, Span, time, Time, TimeSpan} from '../base/time'; 18import {timestampFormat, TimestampFormat} from '../core/timestamp_format'; 19 20import { 21 BACKGROUND_COLOR, 22 FOREGROUND_COLOR, 23 TRACK_SHELL_WIDTH, 24} from './css_constants'; 25import {globals} from './globals'; 26import { 27 getMaxMajorTicks, 28 TickGenerator, 29 TickType, 30 timeScaleForVisibleWindow, 31} from './gridline_helper'; 32import {PanelSize} from './panel'; 33import {Panel} from './panel_container'; 34import {renderDuration} from './widgets/duration'; 35 36export interface BBox { 37 x: number; 38 y: number; 39 width: number; 40 height: number; 41} 42 43// Draws a vertical line with two horizontal tails at the left and right and 44// a label in the middle. It looks a bit like a stretched H: 45// |--- Label ---| 46// The |target| bounding box determines where to draw the H. 47// The |bounds| bounding box gives the visible region, this is used to adjust 48// the positioning of the label to ensure it is on screen. 49function drawHBar( 50 ctx: CanvasRenderingContext2D, 51 target: BBox, 52 bounds: BBox, 53 label: string, 54) { 55 ctx.fillStyle = FOREGROUND_COLOR; 56 57 const xLeft = Math.floor(target.x); 58 const xRight = Math.floor(target.x + target.width); 59 const yMid = Math.floor(target.height / 2 + target.y); 60 const xWidth = xRight - xLeft; 61 62 // Don't draw in the track shell. 63 ctx.beginPath(); 64 ctx.rect(bounds.x, bounds.y, bounds.width, bounds.height); 65 ctx.clip(); 66 67 // Draw horizontal bar of the H. 68 ctx.fillRect(xLeft, yMid, xWidth, 1); 69 // Draw left vertical bar of the H. 70 ctx.fillRect(xLeft, target.y, 1, target.height); 71 // Draw right vertical bar of the H. 72 ctx.fillRect(xRight, target.y, 1, target.height); 73 74 const labelWidth = ctx.measureText(label).width; 75 76 // Find a good position for the label: 77 // By default put the label in the middle of the H: 78 let labelXLeft = Math.floor(xWidth / 2 - labelWidth / 2 + xLeft); 79 80 if ( 81 labelWidth > target.width || 82 labelXLeft < bounds.x || 83 labelXLeft + labelWidth > bounds.x + bounds.width 84 ) { 85 // It won't fit in the middle or would be at least partly out of bounds 86 // so put it either to the left or right: 87 if (xRight > bounds.x + bounds.width) { 88 // If the H extends off the right side of the screen the label 89 // goes on the left of the H. 90 labelXLeft = xLeft - labelWidth - 3; 91 } else { 92 // Otherwise the label goes on the right of the H. 93 labelXLeft = xRight + 3; 94 } 95 } 96 97 ctx.fillStyle = BACKGROUND_COLOR; 98 ctx.fillRect(labelXLeft - 1, 0, labelWidth + 1, target.height); 99 100 ctx.textBaseline = 'middle'; 101 ctx.fillStyle = FOREGROUND_COLOR; 102 ctx.font = '10px Roboto Condensed'; 103 ctx.fillText(label, labelXLeft, yMid); 104} 105 106function drawIBar( 107 ctx: CanvasRenderingContext2D, 108 xPos: number, 109 bounds: BBox, 110 label: string, 111) { 112 if (xPos < bounds.x) return; 113 114 ctx.fillStyle = FOREGROUND_COLOR; 115 ctx.fillRect(xPos, 0, 1, bounds.width); 116 117 const yMid = Math.floor(bounds.height / 2 + bounds.y); 118 const labelWidth = ctx.measureText(label).width; 119 const padding = 3; 120 121 let xPosLabel; 122 if (xPos + padding + labelWidth > bounds.width) { 123 xPosLabel = xPos - padding; 124 ctx.textAlign = 'right'; 125 } else { 126 xPosLabel = xPos + padding; 127 ctx.textAlign = 'left'; 128 } 129 130 ctx.fillStyle = BACKGROUND_COLOR; 131 ctx.fillRect(xPosLabel - 1, 0, labelWidth + 2, bounds.height); 132 133 ctx.textBaseline = 'middle'; 134 ctx.fillStyle = FOREGROUND_COLOR; 135 ctx.font = '10px Roboto Condensed'; 136 ctx.fillText(label, xPosLabel, yMid); 137} 138 139export class TimeSelectionPanel implements Panel { 140 readonly kind = 'panel'; 141 readonly selectable = false; 142 143 render(): m.Children { 144 return m('.time-selection-panel'); 145 } 146 147 renderCanvas(ctx: CanvasRenderingContext2D, size: PanelSize) { 148 ctx.fillStyle = '#999'; 149 ctx.fillRect(TRACK_SHELL_WIDTH - 2, 0, 2, size.height); 150 151 ctx.save(); 152 ctx.beginPath(); 153 ctx.rect(TRACK_SHELL_WIDTH, 0, size.width - TRACK_SHELL_WIDTH, size.height); 154 ctx.clip(); 155 156 const span = globals.timeline.visibleTimeSpan; 157 if (size.width > TRACK_SHELL_WIDTH && span.duration > 0n) { 158 const maxMajorTicks = getMaxMajorTicks(size.width - TRACK_SHELL_WIDTH); 159 const map = timeScaleForVisibleWindow(TRACK_SHELL_WIDTH, size.width); 160 161 const offset = globals.timestampOffset(); 162 const tickGen = new TickGenerator(span, maxMajorTicks, offset); 163 for (const {type, time} of tickGen) { 164 const px = Math.floor(map.timeToPx(time)); 165 if (type === TickType.MAJOR) { 166 ctx.fillRect(px, 0, 1, size.height); 167 } 168 } 169 } 170 171 const localArea = globals.timeline.selectedArea; 172 const selection = globals.state.selection; 173 if (localArea !== undefined) { 174 const start = Time.min(localArea.start, localArea.end); 175 const end = Time.max(localArea.start, localArea.end); 176 this.renderSpan(ctx, size, new TimeSpan(start, end)); 177 } else if (selection.kind === 'area') { 178 const start = Time.min(selection.start, selection.end); 179 const end = Time.max(selection.start, selection.end); 180 this.renderSpan(ctx, size, new TimeSpan(start, end)); 181 } 182 183 if (globals.state.hoverCursorTimestamp !== -1n) { 184 this.renderHover(ctx, size, globals.state.hoverCursorTimestamp); 185 } 186 187 for (const note of Object.values(globals.state.notes)) { 188 const noteIsSelected = 189 selection.kind === 'note' && selection.id === note.id; 190 if (note.noteType === 'SPAN' && !noteIsSelected) { 191 this.renderSpan(ctx, size, new TimeSpan(note.start, note.end)); 192 } 193 } 194 195 ctx.restore(); 196 } 197 198 renderHover(ctx: CanvasRenderingContext2D, size: PanelSize, ts: time) { 199 const {visibleTimeScale} = globals.timeline; 200 const xPos = TRACK_SHELL_WIDTH + Math.floor(visibleTimeScale.timeToPx(ts)); 201 const domainTime = globals.toDomainTime(ts); 202 const label = stringifyTimestamp(domainTime); 203 drawIBar(ctx, xPos, this.bounds(size), label); 204 } 205 206 renderSpan( 207 ctx: CanvasRenderingContext2D, 208 size: PanelSize, 209 span: Span<time, duration>, 210 ) { 211 const {visibleTimeScale} = globals.timeline; 212 const xLeft = visibleTimeScale.timeToPx(span.start); 213 const xRight = visibleTimeScale.timeToPx(span.end); 214 const label = renderDuration(span.duration); 215 drawHBar( 216 ctx, 217 { 218 x: TRACK_SHELL_WIDTH + xLeft, 219 y: 0, 220 width: xRight - xLeft, 221 height: size.height, 222 }, 223 this.bounds(size), 224 label, 225 ); 226 } 227 228 private bounds(size: PanelSize): BBox { 229 return { 230 x: TRACK_SHELL_WIDTH, 231 y: 0, 232 width: size.width - TRACK_SHELL_WIDTH, 233 height: size.height, 234 }; 235 } 236} 237 238function stringifyTimestamp(time: time): string { 239 const fmt = timestampFormat(); 240 switch (fmt) { 241 case TimestampFormat.UTC: 242 case TimestampFormat.TraceTz: 243 case TimestampFormat.Timecode: 244 const THIN_SPACE = '\u2009'; 245 return Time.toTimecode(time).toString(THIN_SPACE); 246 case TimestampFormat.Raw: 247 return time.toString(); 248 case TimestampFormat.RawLocale: 249 return time.toLocaleString(); 250 case TimestampFormat.Seconds: 251 return Time.formatSeconds(time); 252 default: 253 const z: never = fmt; 254 throw new Error(`Invalid timestamp ${z}`); 255 } 256} 257