blob: f890d364085e82c7356ce1b701eda92267283bfb [file] [log] [blame]
// Copyright (C) 2018 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import m from 'mithril';
import {assertExists} from '../base/logging';
import {EngineProxy} from '../common/engine';
import {HighPrecisionTimeSpan} from '../common/high_precision_time';
import {TrackState} from '../common/state';
import {TPTime} from '../common/time';
import {TrackData} from '../common/track_data';
import {checkerboard} from './checkerboard';
import {globals} from './globals';
import {PxSpan, TimeScale} from './time_scale';
import {TrackButtonAttrs} from './track_panel';
// Args passed to the track constructors when creating a new track.
export interface NewTrackArgs {
trackId: string;
engine: EngineProxy;
}
// This interface forces track implementations to have some static properties.
// Typescript does not have abstract static members, which is why this needs to
// be in a separate interface.
export interface TrackCreator {
// Store the kind explicitly as a string as opposed to using class.kind in
// case we ever minify our code.
readonly kind: string;
// We need the |create| method because the stored value in the registry can be
// an abstract class, and we cannot call 'new' on an abstract class.
create(args: NewTrackArgs): Track;
}
export interface SliceRect {
left: number;
width: number;
top: number;
height: number;
visible: boolean;
}
// The abstract class that needs to be implemented by all tracks.
export abstract class Track<Config = {}, Data extends TrackData = TrackData> {
// The UI-generated track ID (not to be confused with the SQL track.id).
protected readonly trackId: string;
protected readonly engine: EngineProxy;
// When true this is a new controller-less track type.
// TODO(hjd): eventually all tracks will be controller-less and this
// should be removed then.
protected frontendOnly = false;
// Caches the last state.track[this.trackId]. This is to deal with track
// deletion, see comments in trackState() below.
private lastTrackState: TrackState;
constructor(args: NewTrackArgs) {
this.trackId = args.trackId;
this.engine = args.engine;
this.lastTrackState = assertExists(globals.state.tracks[this.trackId]);
}
// Last call the track will receive. Called just before the last reference to
// this object is removed.
onDestroy() {}
protected abstract renderCanvas(ctx: CanvasRenderingContext2D): void;
protected get trackState(): TrackState {
// We can end up in a state where a Track is still in the mithril renderer
// tree but its corresponding state has been deleted. This can happen in the
// interval of time between a track being removed from the state and the
// next animation frame that would remove the Track object. If a mouse event
// is dispatched in the meanwhile (or a promise is resolved), we need to be
// able to access the state. Hence the caching logic here.
const trackState = globals.state.tracks[this.trackId];
if (trackState === undefined) {
return this.lastTrackState;
}
this.lastTrackState = trackState;
return trackState;
}
get config(): Config {
return this.trackState.config as Config;
}
data(): Data|undefined {
if (this.frontendOnly) {
return undefined;
}
return globals.trackDataStore.get(this.trackId) as Data;
}
getHeight(): number {
return 40;
}
getTrackShellButtons(): Array<m.Vnode<TrackButtonAttrs>> {
return [];
}
getContextMenu(): m.Vnode<any>|null {
return null;
}
onMouseMove(_position: {x: number, y: number}) {}
// Returns whether the mouse click has selected something.
// Used to prevent further propagation if necessary.
onMouseClick(_position: {x: number, y: number}): boolean {
return false;
}
onMouseOut(): void {}
onFullRedraw(): void {}
render(ctx: CanvasRenderingContext2D) {
globals.frontendLocalState.addVisibleTrack(this.trackState.id);
if (this.data() === undefined && !this.frontendOnly) {
const {visibleWindowTime, visibleTimeScale} = globals.frontendLocalState;
const startPx =
Math.floor(visibleTimeScale.hpTimeToPx(visibleWindowTime.start));
const endPx =
Math.ceil(visibleTimeScale.hpTimeToPx(visibleWindowTime.end));
checkerboard(ctx, this.getHeight(), startPx, endPx);
} else {
this.renderCanvas(ctx);
}
}
drawTrackHoverTooltip(
ctx: CanvasRenderingContext2D, pos: {x: number, y: number}, text: string,
text2?: string) {
ctx.font = '10px Roboto Condensed';
ctx.textBaseline = 'middle';
ctx.textAlign = 'left';
// TODO(hjd): Avoid measuring text all the time (just use monospace?)
const textMetrics = ctx.measureText(text);
const text2Metrics = ctx.measureText(text2 || '');
// Padding on each side of the box containing the tooltip:
const paddingPx = 4;
// Figure out the width of the tool tip box:
let width = Math.max(textMetrics.width, text2Metrics.width);
width += paddingPx * 2;
// and the height:
let height = 0;
height += textMetrics.fontBoundingBoxAscent;
height += textMetrics.fontBoundingBoxDescent;
if (text2 !== undefined) {
height += text2Metrics.fontBoundingBoxAscent;
height += text2Metrics.fontBoundingBoxDescent;
}
height += paddingPx * 2;
let x = pos.x;
let y = pos.y;
// Move box to the top right of the mouse:
x += 10;
y -= 10;
// Ensure the box is on screen:
const endPx = globals.frontendLocalState.visibleTimeScale.pxSpan.end;
if (x + width > endPx) {
x -= x + width - endPx;
}
if (y < 0) {
y = 0;
}
if (y + height > this.getHeight()) {
y -= y + height - this.getHeight();
}
// Draw everything:
ctx.fillStyle = 'rgba(255, 255, 255, 0.9)';
ctx.fillRect(x, y, width, height);
ctx.fillStyle = 'hsl(200, 50%, 40%)';
ctx.fillText(
text, x + paddingPx, y + paddingPx + textMetrics.fontBoundingBoxAscent);
if (text2 !== undefined) {
const yOffsetPx = textMetrics.fontBoundingBoxAscent +
textMetrics.fontBoundingBoxDescent +
text2Metrics.fontBoundingBoxAscent;
ctx.fillText(text2, x + paddingPx, y + paddingPx + yOffsetPx);
}
}
// Returns a place where a given slice should be drawn. Should be implemented
// only for track types that support slices e.g. chrome_slice, async_slices
// tStart - slice start time in seconds, tEnd - slice end time in seconds,
// depth - slice depth
getSliceRect(
_visibleTimeScale: TimeScale, _visibleWindowTime: HighPrecisionTimeSpan,
_windowSpan: PxSpan, _tStart: TPTime, _tEnd: TPTime,
_depth: number): SliceRect|undefined {
return undefined;
}
}