1/* 2 * Copyright (C) 2022 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17import {Color} from 'app/colors'; 18import {Point} from 'common/geometry_types'; 19import {MouseEventButton} from 'common/mouse_event_button'; 20import {Padding} from 'common/padding'; 21import {Timestamp} from 'common/time'; 22import {Trace} from 'trace/trace'; 23import {TRACE_INFO} from 'trace/trace_info'; 24import {CanvasMouseHandler} from './canvas_mouse_handler'; 25import {CanvasMouseHandlerImpl} from './canvas_mouse_handler_impl'; 26import {DraggableCanvasObject} from './draggable_canvas_object'; 27import {DraggableCanvasObjectImpl} from './draggable_canvas_object_impl'; 28import { 29 MiniCanvasDrawerData, 30 TimelineTrace, 31 TimelineTraces, 32} from './mini_canvas_drawer_data'; 33import {MiniTimelineDrawer} from './mini_timeline_drawer'; 34import {MiniTimelineDrawerInput} from './mini_timeline_drawer_input'; 35 36/** 37 * Mini timeline drawer implementation 38 * @docs-private 39 */ 40export class MiniTimelineDrawerImpl implements MiniTimelineDrawer { 41 ctx: CanvasRenderingContext2D; 42 handler: CanvasMouseHandler; 43 private activePointer: DraggableCanvasObject; 44 private lastMousePoint: Point | undefined; 45 private static readonly MARKER_CLICK_REGION_WIDTH = 2; 46 47 constructor( 48 public canvas: HTMLCanvasElement, 49 private inputGetter: () => MiniTimelineDrawerInput, 50 private onPointerPositionDragging: (pos: Timestamp) => void, 51 private onPointerPositionChanged: (pos: Timestamp) => void, 52 private onUnhandledClick: ( 53 pos: Timestamp, 54 trace: Trace<object> | undefined, 55 ) => void, 56 ) { 57 const ctx = canvas.getContext('2d'); 58 59 if (ctx === null) { 60 throw Error('MiniTimeline canvas context was null!'); 61 } 62 63 this.ctx = ctx; 64 65 const onUnhandledClickInternal = async ( 66 mousePoint: Point, 67 button: number, 68 trace: Trace<object> | undefined, 69 ) => { 70 if (button === MouseEventButton.SECONDARY) { 71 return; 72 } 73 let pointX = mousePoint.x; 74 75 if (mousePoint.y < this.getMarkerHeight()) { 76 pointX = 77 this.getInput().bookmarks.find((bm) => { 78 const diff = mousePoint.x - bm; 79 return diff > 0 && diff < this.getMarkerMaxWidth(); 80 }) ?? mousePoint.x; 81 } 82 83 this.onUnhandledClick( 84 this.getInput().transformer.untransform(pointX), 85 trace, 86 ); 87 }; 88 this.handler = new CanvasMouseHandlerImpl( 89 this, 90 'pointer', 91 onUnhandledClickInternal, 92 ); 93 94 this.activePointer = new DraggableCanvasObjectImpl( 95 this, 96 () => this.getSelectedPosition(), 97 (ctx: CanvasRenderingContext2D, position: number) => { 98 const barWidth = 3; 99 const triangleHeight = this.getMarkerHeight(); 100 101 ctx.beginPath(); 102 ctx.moveTo(position - triangleHeight, 0); 103 ctx.lineTo(position + triangleHeight, 0); 104 ctx.lineTo(position + barWidth / 2, triangleHeight); 105 ctx.lineTo(position + barWidth / 2, this.getHeight()); 106 ctx.lineTo(position - barWidth / 2, this.getHeight()); 107 ctx.lineTo(position - barWidth / 2, triangleHeight); 108 ctx.closePath(); 109 }, 110 { 111 fillStyle: Color.ACTIVE_POINTER, 112 fill: true, 113 }, 114 (x) => { 115 const input = this.getInput(); 116 input.selectedPosition = x; 117 this.onPointerPositionDragging(input.transformer.untransform(x)); 118 }, 119 (x) => { 120 const input = this.getInput(); 121 input.selectedPosition = x; 122 this.onPointerPositionChanged(input.transformer.untransform(x)); 123 }, 124 () => this.getUsableRange(), 125 ); 126 } 127 128 getXScale() { 129 return this.ctx.getTransform().m11; 130 } 131 132 getYScale() { 133 return this.ctx.getTransform().m22; 134 } 135 136 getWidth() { 137 return this.canvas.width / this.getXScale(); 138 } 139 140 getHeight() { 141 return this.canvas.height / this.getYScale(); 142 } 143 144 getUsableRange() { 145 const padding = this.getPadding(); 146 return { 147 from: padding.left, 148 to: this.getWidth() - padding.left - padding.right, 149 }; 150 } 151 152 getInput(): MiniCanvasDrawerData { 153 return this.inputGetter().transform(this.getUsableRange()); 154 } 155 156 getClickRange(clickPos: Point) { 157 const markerHeight = this.getMarkerHeight(); 158 if (clickPos.y > markerHeight) { 159 return { 160 from: clickPos.x - MiniTimelineDrawerImpl.MARKER_CLICK_REGION_WIDTH, 161 to: clickPos.x + MiniTimelineDrawerImpl.MARKER_CLICK_REGION_WIDTH, 162 }; 163 } 164 const markerMaxWidth = this.getMarkerMaxWidth(); 165 return { 166 from: clickPos.x - markerMaxWidth, 167 to: clickPos.x + markerMaxWidth, 168 }; 169 } 170 171 getSelectedPosition() { 172 return this.getInput().selectedPosition; 173 } 174 175 getBookmarks(): number[] { 176 return this.getInput().bookmarks; 177 } 178 179 async getTimelineTraces(): Promise<TimelineTraces> { 180 return await this.getInput().getTimelineTraces(); 181 } 182 183 getPadding(): Padding { 184 const height = this.getHeight(); 185 const pointerWidth = this.getPointerWidth(); 186 return { 187 top: Math.ceil(height / 10), 188 bottom: Math.ceil(height / 10), 189 left: Math.ceil(pointerWidth / 2), 190 right: Math.ceil(pointerWidth / 2), 191 }; 192 } 193 194 getInnerHeight() { 195 const padding = this.getPadding(); 196 return this.getHeight() - padding.top - padding.bottom; 197 } 198 199 async draw() { 200 this.ctx.clearRect(0, 0, this.getWidth(), this.getHeight()); 201 await this.drawTraceLines(); 202 this.drawBookmarks(); 203 this.activePointer.draw(this.ctx); 204 this.drawHoverCursor(); 205 } 206 207 async updateHover(mousePoint: Point | undefined) { 208 this.lastMousePoint = mousePoint; 209 await this.draw(); 210 } 211 212 async getTraceClicked(mousePoint: Point): Promise<Trace<object> | undefined> { 213 const timelineTraces = await this.getTimelineTraces(); 214 const innerHeight = this.getInnerHeight(); 215 const lineHeight = this.getLineHeight(timelineTraces, innerHeight); 216 let fromTop = this.getPadding().top + innerHeight - lineHeight; 217 218 for (const trace of timelineTraces.keys()) { 219 if ( 220 this.pointWithinTimeline(mousePoint.y, fromTop, fromTop + lineHeight) 221 ) { 222 return trace; 223 } 224 fromTop -= this.fromTopStep(lineHeight); 225 } 226 227 return undefined; 228 } 229 230 private getPointerWidth() { 231 return this.getHeight() / 6; 232 } 233 234 private getMarkerMaxWidth() { 235 return (this.getPointerWidth() * 2) / 3; 236 } 237 238 private getMarkerHeight() { 239 return this.getPointerWidth() / 2; 240 } 241 242 private async drawTraceLines() { 243 const timelineTraces = await this.getTimelineTraces(); 244 const innerHeight = this.getInnerHeight(); 245 const lineHeight = this.getLineHeight(timelineTraces, innerHeight); 246 let fromTop = this.getPadding().top + innerHeight - lineHeight; 247 248 timelineTraces.forEach((timelineTrace, trace) => { 249 if (this.inputGetter().timelineData.getActiveTrace() === trace) { 250 this.fillActiveTimelineBackground(fromTop, lineHeight); 251 } else if ( 252 this.lastMousePoint?.y && 253 this.pointWithinTimeline(this.lastMousePoint?.y, fromTop, lineHeight) 254 ) { 255 this.fillHoverTimelineBackground(fromTop, lineHeight); 256 } 257 258 this.drawTraceEntries(trace, timelineTrace, fromTop, lineHeight); 259 260 fromTop -= this.fromTopStep(lineHeight); 261 }); 262 } 263 264 private drawTraceEntries( 265 trace: Trace<object>, 266 timelineTrace: TimelineTrace, 267 fromTop: number, 268 lineHeight: number, 269 ) { 270 this.ctx.globalAlpha = 0.7; 271 this.ctx.fillStyle = TRACE_INFO[trace.type].color; 272 this.ctx.strokeStyle = 'blue'; 273 274 for (const entry of timelineTrace.points) { 275 const width = 5; 276 this.ctx.fillRect(entry - width / 2, fromTop, width, lineHeight); 277 } 278 279 for (const entry of timelineTrace.segments) { 280 const width = Math.max(entry.to - entry.from, 3); 281 this.ctx.fillRect(entry.from, fromTop, width, lineHeight); 282 } 283 284 this.ctx.fillStyle = Color.ACTIVE_POINTER; 285 if (timelineTrace.activePoint) { 286 const entry = timelineTrace.activePoint; 287 const width = 5; 288 this.ctx.fillRect(entry - width / 2, fromTop, width, lineHeight); 289 } 290 291 if (timelineTrace.activeSegment) { 292 const entry = timelineTrace.activeSegment; 293 const width = Math.max(entry.to - entry.from, 3); 294 this.ctx.fillRect(entry.from, fromTop, width, lineHeight); 295 } 296 297 this.ctx.globalAlpha = 1.0; 298 } 299 300 private drawHoverCursor() { 301 if (!this.lastMousePoint) { 302 return; 303 } 304 const hoverWidth = 2; 305 this.ctx.beginPath(); 306 this.ctx.moveTo(this.lastMousePoint.x - hoverWidth / 2, 0); 307 this.ctx.lineTo(this.lastMousePoint.x + hoverWidth / 2, 0); 308 this.ctx.lineTo(this.lastMousePoint.x + hoverWidth / 2, this.getHeight()); 309 this.ctx.lineTo(this.lastMousePoint.x - hoverWidth / 2, this.getHeight()); 310 this.ctx.closePath(); 311 312 this.ctx.globalAlpha = 0.4; 313 this.ctx.fillStyle = Color.ACTIVE_POINTER; 314 this.ctx.fill(); 315 this.ctx.globalAlpha = 1.0; 316 } 317 318 private drawBookmarks() { 319 this.getBookmarks().forEach((position) => { 320 const flagWidth = this.getMarkerMaxWidth(); 321 const flagHeight = this.getMarkerHeight(); 322 const barWidth = 2; 323 324 this.ctx.beginPath(); 325 this.ctx.moveTo(position - barWidth / 2, 0); 326 this.ctx.lineTo(position + flagWidth, 0); 327 this.ctx.lineTo(position + (flagWidth * 5) / 6, flagHeight / 2); 328 this.ctx.lineTo(position + flagWidth, flagHeight); 329 this.ctx.lineTo(position + barWidth / 2, flagHeight); 330 this.ctx.lineTo(position + barWidth / 2, this.getHeight()); 331 this.ctx.lineTo(position - barWidth / 2, this.getHeight()); 332 this.ctx.closePath(); 333 334 this.ctx.fillStyle = Color.BOOKMARK; 335 this.ctx.fill(); 336 }); 337 } 338 339 private fromTopStep(lineHeight: number): number { 340 return (lineHeight * 4) / 3; 341 } 342 343 private fillActiveTimelineBackground(fromTop: number, lineHeight: number) { 344 this.ctx.globalAlpha = 1.0; 345 this.ctx.fillStyle = this.inputGetter().isDarkMode ? '#696563' : '#eeeff0'; // Keep in sync with var(--drawer-block-primary) in material-theme.scss; 346 this.ctx.fillRect(0, fromTop, this.getUsableRange().to, lineHeight); 347 } 348 349 private fillHoverTimelineBackground(fromTop: number, lineHeight: number) { 350 this.ctx.globalAlpha = 1.0; 351 this.ctx.fillStyle = this.inputGetter().isDarkMode ? '#4e5767' : '#E8F0FE'; // Keep in sync with var(--hover-element-color) in material-theme.scss; 352 this.ctx.fillRect(0, fromTop, this.getUsableRange().to, lineHeight); 353 } 354 355 private getLineHeight( 356 timelineTraces: TimelineTraces, 357 innerHeight: number, 358 ): number { 359 return innerHeight / (Math.max(timelineTraces.size - 10, 0) + 12); 360 } 361 362 private pointWithinTimeline( 363 point: number, 364 from: number, 365 to: number, 366 ): boolean { 367 return point > from && point <= from + to; 368 } 369} 370