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'; 16 17import {assertExists} from '../base/logging'; 18import {Engine} from '../common/engine'; 19import {TrackState} from '../common/state'; 20import {TrackData} from '../common/track_data'; 21 22import {checkerboard} from './checkerboard'; 23import {globals} from './globals'; 24import {TrackButtonAttrs} from './track_panel'; 25 26/** 27 * Args passed to the track constructors when creating a new track. 28 */ 29export interface NewTrackArgs { 30 trackId: string; 31 engine: Engine; 32} 33 34/** 35 * This interface forces track implementations to have some static properties. 36 * Typescript does not have abstract static members, which is why this needs to 37 * be in a separate interface. 38 */ 39export interface TrackCreator { 40 // Store the kind explicitly as a string as opposed to using class.kind in 41 // case we ever minify our code. 42 readonly kind: string; 43 44 // We need the |create| method because the stored value in the registry can be 45 // an abstract class, and we cannot call 'new' on an abstract class. 46 create(args: NewTrackArgs): Track; 47} 48 49export interface SliceRect { 50 left: number; 51 width: number; 52 top: number; 53 height: number; 54 visible: boolean; 55} 56 57/** 58 * The abstract class that needs to be implemented by all tracks. 59 */ 60export abstract class Track<Config = {}, Data extends TrackData = TrackData> { 61 // The UI-generated track ID (not to be confused with the SQL track.id). 62 protected readonly trackId: string; 63 protected readonly engine: Engine; 64 65 // When true this is a new controller-less track type. 66 // TODO(hjd): eventually all tracks will be controller-less and this 67 // should be removed then. 68 protected frontendOnly = false; 69 70 // Caches the last state.track[this.trackId]. This is to deal with track 71 // deletion, see comments in trackState() below. 72 private lastTrackState: TrackState; 73 74 constructor(args: NewTrackArgs) { 75 this.trackId = args.trackId; 76 this.engine = args.engine; 77 this.lastTrackState = assertExists(globals.state.tracks[this.trackId]); 78 } 79 80 // Last call the track will receive. Called just before the last reference to 81 // this object is removed. 82 onDestroy() {} 83 84 protected abstract renderCanvas(ctx: CanvasRenderingContext2D): void; 85 86 protected get trackState(): TrackState { 87 // We can end up in a state where a Track is still in the mithril renderer 88 // tree but its corresponding state has been deleted. This can happen in the 89 // interval of time between a track being removed from the state and the 90 // next animation frame that would remove the Track object. If a mouse event 91 // is dispatched in the meanwhile (or a promise is resolved), we need to be 92 // able to access the state. Hence the caching logic here. 93 const trackState = globals.state.tracks[this.trackId]; 94 if (trackState === undefined) { 95 return this.lastTrackState; 96 } 97 this.lastTrackState = trackState; 98 return trackState; 99 } 100 101 get config(): Config { 102 return this.trackState.config as Config; 103 } 104 105 data(): Data|undefined { 106 if (this.frontendOnly) { 107 return undefined; 108 } 109 return globals.trackDataStore.get(this.trackId) as Data; 110 } 111 112 getHeight(): number { 113 return 40; 114 } 115 116 getTrackShellButtons(): Array<m.Vnode<TrackButtonAttrs>> { 117 return []; 118 } 119 120 onMouseMove(_position: {x: number, y: number}) {} 121 122 /** 123 * Returns whether the mouse click has selected something. 124 * Used to prevent further propagation if necessary. 125 */ 126 onMouseClick(_position: {x: number, y: number}): boolean { 127 return false; 128 } 129 130 onMouseOut(): void {} 131 132 onFullRedraw(): void {} 133 134 render(ctx: CanvasRenderingContext2D) { 135 globals.frontendLocalState.addVisibleTrack(this.trackState.id); 136 if (this.data() === undefined && !this.frontendOnly) { 137 const {visibleWindowTime, timeScale} = globals.frontendLocalState; 138 const startPx = Math.floor(timeScale.timeToPx(visibleWindowTime.start)); 139 const endPx = Math.ceil(timeScale.timeToPx(visibleWindowTime.end)); 140 checkerboard(ctx, this.getHeight(), startPx, endPx); 141 } else { 142 this.renderCanvas(ctx); 143 } 144 } 145 146 drawTrackHoverTooltip( 147 ctx: CanvasRenderingContext2D, pos: {x: number, y: number}, text: string, 148 text2?: string) { 149 ctx.font = '10px Roboto Condensed'; 150 ctx.textBaseline = 'middle'; 151 ctx.textAlign = 'left'; 152 153 // TODO(hjd): Avoid measuring text all the time (just use monospace?) 154 const textMetrics = ctx.measureText(text); 155 const text2Metrics = ctx.measureText(text2 || ''); 156 157 // Padding on each side of the box containing the tooltip: 158 const paddingPx = 4; 159 160 // Figure out the width of the tool tip box: 161 let width = Math.max(textMetrics.width, text2Metrics.width); 162 width += paddingPx * 2; 163 164 // and the height: 165 let height = 0; 166 height += textMetrics.fontBoundingBoxAscent; 167 height += textMetrics.fontBoundingBoxDescent; 168 if (text2 !== undefined) { 169 height += text2Metrics.fontBoundingBoxAscent; 170 height += text2Metrics.fontBoundingBoxDescent; 171 } 172 height += paddingPx * 2; 173 174 let x = pos.x; 175 let y = pos.y; 176 177 // Move box to the top right of the mouse: 178 x += 10; 179 y -= 10; 180 181 // Ensure the box is on screen: 182 const endPx = globals.frontendLocalState.timeScale.endPx; 183 if (x + width > endPx) { 184 x -= x + width - endPx; 185 } 186 if (y < 0) { 187 y = 0; 188 } 189 if (y + height > this.getHeight()) { 190 y -= y + height - this.getHeight(); 191 } 192 193 // Draw everything: 194 ctx.fillStyle = 'rgba(255, 255, 255, 0.9)'; 195 ctx.fillRect(x, y, width, height); 196 197 ctx.fillStyle = 'hsl(200, 50%, 40%)'; 198 ctx.fillText( 199 text, x + paddingPx, y + paddingPx + textMetrics.fontBoundingBoxAscent); 200 if (text2 !== undefined) { 201 const yOffsetPx = textMetrics.fontBoundingBoxAscent + 202 textMetrics.fontBoundingBoxDescent + 203 text2Metrics.fontBoundingBoxAscent; 204 ctx.fillText(text2, x + paddingPx, y + paddingPx + yOffsetPx); 205 } 206 } 207 208 /** 209 * Returns a place where a given slice should be drawn. Should be implemented 210 * only for track types that support slices e.g. chrome_slice, async_slices 211 * tStart - slice start time in seconds, tEnd - slice end time in seconds, 212 * depth - slice depth 213 */ 214 getSliceRect(_tStart: number, _tEnd: number, _depth: number): SliceRect 215 |undefined { 216 return undefined; 217 } 218} 219