| // 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 * as 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<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: 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]); |
| } |
| |
| 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 []; |
| } |
| |
| 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, timeScale} = globals.frontendLocalState; |
| const startPx = Math.floor(timeScale.timeToPx(visibleWindowTime.start)); |
| const endPx = Math.ceil(timeScale.timeToPx(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.timeScale.endPx; |
| 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; |
| } |
| } |