1// Copyright (C) 2018 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 * as m from 'mithril'; 16import {TrackState} from '../common/state'; 17import {TrackData} from '../common/track_data'; 18import {checkerboard} from './checkerboard'; 19 20import {globals} from './globals'; 21import {TrackButtonAttrs} from './track_panel'; 22 23/** 24 * This interface forces track implementations to have some static properties. 25 * Typescript does not have abstract static members, which is why this needs to 26 * be in a separate interface. 27 */ 28export interface TrackCreator { 29 // Store the kind explicitly as a string as opposed to using class.kind in 30 // case we ever minify our code. 31 readonly kind: string; 32 33 // We need the |create| method because the stored value in the registry is an 34 // abstract class, and we cannot call 'new' on an abstract class. 35 create(TrackState: TrackState): Track; 36} 37 38/** 39 * The abstract class that needs to be implemented by all tracks. 40 */ 41export abstract class Track<Config = {}, Data extends TrackData = TrackData> { 42 constructor(protected trackState: TrackState) {} 43 protected abstract renderCanvas(ctx: CanvasRenderingContext2D): void; 44 45 get config(): Config { 46 return this.trackState.config as Config; 47 } 48 49 data(): Data|undefined { 50 return globals.trackDataStore.get(this.trackState.id) as Data; 51 } 52 53 getHeight(): number { 54 return 40; 55 } 56 57 getTrackShellButtons(): Array<m.Vnode<TrackButtonAttrs>> { 58 return []; 59 } 60 61 onMouseMove(_position: {x: number, y: number}) {} 62 63 /** 64 * Returns whether the mouse click has selected something. 65 * Used to prevent further propagation if necessary. 66 */ 67 onMouseClick(_position: {x: number, y: number}): boolean { 68 return false; 69 } 70 71 onMouseOut() {} 72 73 render(ctx: CanvasRenderingContext2D) { 74 globals.frontendLocalState.addVisibleTrack(this.trackState.id); 75 if (this.data() === undefined) { 76 const {visibleWindowTime, timeScale} = globals.frontendLocalState; 77 const startPx = Math.floor(timeScale.timeToPx(visibleWindowTime.start)); 78 const endPx = Math.ceil(timeScale.timeToPx(visibleWindowTime.end)); 79 checkerboard(ctx, this.getHeight(), startPx, endPx); 80 } else { 81 this.renderCanvas(ctx); 82 } 83 } 84 85 drawTrackHoverTooltip( 86 ctx: CanvasRenderingContext2D, xPos: number, text: string, 87 text2?: string) { 88 ctx.font = '10px Roboto Condensed'; 89 const textWidth = ctx.measureText(text).width; 90 let width = textWidth; 91 let textYPos = this.getHeight() / 2; 92 93 if (text2 !== undefined) { 94 const text2Width = ctx.measureText(text2).width; 95 width = Math.max(textWidth, text2Width); 96 textYPos = this.getHeight() / 2 - 6; 97 } 98 99 // Move tooltip over if it would go off the right edge of the viewport. 100 const rectWidth = width + 16; 101 const endPx = globals.frontendLocalState.timeScale.endPx; 102 if (xPos + rectWidth > endPx) { 103 xPos -= (xPos + rectWidth - endPx); 104 } 105 106 ctx.fillStyle = 'rgba(255, 255, 255, 0.9)'; 107 const rectMargin = this.getHeight() / 12; 108 ctx.fillRect( 109 xPos, rectMargin, rectWidth, this.getHeight() - rectMargin * 2); 110 ctx.fillStyle = 'hsl(200, 50%, 40%)'; 111 ctx.textAlign = 'left'; 112 ctx.textBaseline = 'middle'; 113 ctx.fillText(text, xPos + 8, textYPos); 114 115 if (text2 !== undefined) { 116 ctx.fillText(text2, xPos + 8, this.getHeight() / 2 + 6); 117 } 118 } 119} 120