// 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 {Engine} from '../common/engine'; import {TrackState} from '../common/state'; import {TrackData} from '../common/track_data'; import {checkerboard} from './checkerboard'; import {globals} from './globals'; import {TrackButtonAttrs} from './track_panel'; // Args passed to the track constructors when creating a new track. export interface NewTrackArgs { trackId: string; engine: Engine; } // 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 { // The UI-generated track ID (not to be confused with the SQL track.id). protected readonly trackId: string; protected readonly engine: Engine; // 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> { return []; } getContextMenu(): m.Vnode|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(_tStart: number, _tEnd: number, _depth: number): SliceRect |undefined { return undefined; } }