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
38export interface SliceRect {
39  left: number;
40  width: number;
41  top: number;
42  height: number;
43  visible: boolean;
44}
45
46/**
47 * The abstract class that needs to be implemented by all tracks.
48 */
49export abstract class Track<Config = {}, Data extends TrackData = TrackData> {
50  private trackId: string;
51  constructor(trackState: TrackState) {
52    this.trackId = trackState.id;
53  }
54  protected abstract renderCanvas(ctx: CanvasRenderingContext2D): void;
55
56  protected get trackState(): TrackState {
57    return globals.state.tracks[this.trackId];
58  }
59
60  get config(): Config {
61    return globals.state.tracks[this.trackId].config as Config;
62  }
63
64  data(): Data|undefined {
65    return globals.trackDataStore.get(this.trackId) as Data;
66  }
67
68  getHeight(): number {
69    return 40;
70  }
71
72  getTrackShellButtons(): Array<m.Vnode<TrackButtonAttrs>> {
73    return [];
74  }
75
76  onMouseMove(_position: {x: number, y: number}) {}
77
78  /**
79   * Returns whether the mouse click has selected something.
80   * Used to prevent further propagation if necessary.
81   */
82  onMouseClick(_position: {x: number, y: number}): boolean {
83    return false;
84  }
85
86  onMouseOut() {}
87
88  render(ctx: CanvasRenderingContext2D) {
89    globals.frontendLocalState.addVisibleTrack(this.trackState.id);
90    if (this.data() === undefined) {
91      const {visibleWindowTime, timeScale} = globals.frontendLocalState;
92      const startPx = Math.floor(timeScale.timeToPx(visibleWindowTime.start));
93      const endPx = Math.ceil(timeScale.timeToPx(visibleWindowTime.end));
94      checkerboard(ctx, this.getHeight(), startPx, endPx);
95    } else {
96      this.renderCanvas(ctx);
97    }
98  }
99
100  drawTrackHoverTooltip(
101      ctx: CanvasRenderingContext2D, xPos: number, text: string,
102      text2?: string) {
103    ctx.font = '10px Roboto Condensed';
104    const textWidth = ctx.measureText(text).width;
105    let width = textWidth;
106    let textYPos = this.getHeight() / 2;
107
108    if (text2 !== undefined) {
109      const text2Width = ctx.measureText(text2).width;
110      width = Math.max(textWidth, text2Width);
111      textYPos = this.getHeight() / 2 - 6;
112    }
113
114    // Move tooltip over if it would go off the right edge of the viewport.
115    const rectWidth = width + 16;
116    const endPx = globals.frontendLocalState.timeScale.endPx;
117    if (xPos + rectWidth > endPx) {
118      xPos -= (xPos + rectWidth - endPx);
119    }
120
121    ctx.fillStyle = 'rgba(255, 255, 255, 0.9)';
122    const rectMargin = this.getHeight() / 12;
123    ctx.fillRect(
124        xPos, rectMargin, rectWidth, this.getHeight() - rectMargin * 2);
125    ctx.fillStyle = 'hsl(200, 50%, 40%)';
126    ctx.textAlign = 'left';
127    ctx.textBaseline = 'middle';
128    ctx.fillText(text, xPos + 8, textYPos);
129
130    if (text2 !== undefined) {
131      ctx.fillText(text2, xPos + 8, this.getHeight() / 2 + 6);
132    }
133  }
134
135  /**
136   * Returns a place where a given slice should be drawn. Should be implemented
137   * only for track types that support slices e.g. chrome_slice, async_slices
138   * tStart - slice start time in seconds, tEnd - slice end time in seconds,
139   * depth - slice depth
140   */
141  getSliceRect(_tStart: number, _tEnd: number, _depth: number): SliceRect
142      |undefined {
143    return undefined;
144  }
145}
146