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