1// Copyright (C) 2019 The Android Open Source Project
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use size 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';
16
17import {timeToString} from '../common/time';
18import {TimeSpan} from '../common/time';
19
20import {TRACK_SHELL_WIDTH} from './css_constants';
21import {globals} from './globals';
22import {gridlines} from './gridline_helper';
23import {Panel, PanelSize} from './panel';
24
25export interface BBox {
26  x: number;
27  y: number;
28  width: number;
29  height: number;
30}
31
32// Draws a vertical line with two horizontal tails at the left and right and
33// a label in the middle. It looks a bit like a stretched H:
34// |--- Label ---|
35// The |target| bounding box determines where to draw the H.
36// The |bounds| bounding box gives the visible region, this is used to adjust
37// the positioning of the label to ensure it is on screen.
38function drawHBar(
39    ctx: CanvasRenderingContext2D, target: BBox, bounds: BBox, label: string) {
40  ctx.fillStyle = '#222';
41
42  const xLeft = Math.floor(target.x);
43  const xRight = Math.ceil(target.x + target.width);
44  const yMid = Math.floor(target.height / 2 + target.y);
45  const xWidth = xRight - xLeft;
46
47  // Don't draw in the track shell.
48  ctx.beginPath();
49  ctx.rect(bounds.x, bounds.y, bounds.width, bounds.height);
50  ctx.clip();
51
52  // Draw horizontal bar of the H.
53  ctx.fillRect(xLeft, yMid, xWidth, 1);
54  // Draw left vertical bar of the H.
55  ctx.fillRect(xLeft, target.y, 1, target.height);
56  // Draw right vertical bar of the H.
57  ctx.fillRect(xRight, target.y, 1, target.height);
58
59  const labelWidth = ctx.measureText(label).width;
60
61  // Find a good position for the label:
62  // By default put the label in the middle of the H:
63  let labelXLeft = Math.floor(xWidth / 2 - labelWidth / 2 + xLeft);
64
65  if (labelWidth > target.width || labelXLeft < bounds.x ||
66      (labelXLeft + labelWidth) > (bounds.x + bounds.width)) {
67    // It won't fit in the middle or would be at least partly out of bounds
68    // so put it either to the left or right:
69    if (xRight > bounds.x + bounds.width) {
70      // If the H extends off the right side of the screen the label
71      // goes on the left of the H.
72      labelXLeft = xLeft - labelWidth - 3;
73    } else {
74      // Otherwise the label goes on the right of the H.
75      labelXLeft = xRight + 3;
76    }
77  }
78
79  ctx.fillStyle = '#ffffff';
80  ctx.fillRect(labelXLeft - 1, 0, labelWidth + 1, target.height);
81
82  ctx.textBaseline = 'middle';
83  ctx.fillStyle = '#222';
84  ctx.font = '10px Roboto Condensed';
85  ctx.fillText(label, labelXLeft, yMid);
86}
87
88function drawIBar(
89    ctx: CanvasRenderingContext2D, xPos: number, bounds: BBox, label: string) {
90  if (xPos < bounds.x) return;
91
92  ctx.fillStyle = '#222';
93  ctx.fillRect(xPos, 0, 1, bounds.width);
94
95  const yMid = Math.floor(bounds.height / 2 + bounds.y);
96  const labelWidth = ctx.measureText(label).width;
97  const padding = 3;
98
99  let xPosLabel;
100  if (xPos + padding + labelWidth > bounds.width) {
101    xPosLabel = xPos - padding;
102    ctx.textAlign = 'right';
103  } else {
104    xPosLabel = xPos + padding;
105    ctx.textAlign = 'left';
106  }
107
108  ctx.fillStyle = '#ffffff';
109  ctx.fillRect(xPosLabel - 1, 0, labelWidth + 2, bounds.height);
110
111  ctx.textBaseline = 'middle';
112  ctx.fillStyle = '#222';
113  ctx.font = '10px Roboto Condensed';
114  ctx.fillText(label, xPosLabel, yMid);
115}
116
117export class TimeSelectionPanel extends Panel {
118  view() {
119    return m('.time-selection-panel');
120  }
121
122  renderCanvas(ctx: CanvasRenderingContext2D, size: PanelSize) {
123    const range = globals.frontendLocalState.visibleWindowTime;
124    const timeScale = globals.frontendLocalState.timeScale;
125
126    ctx.fillStyle = '#999';
127    ctx.fillRect(TRACK_SHELL_WIDTH - 2, 0, 2, size.height);
128    for (const xAndTime of gridlines(size.width, range, timeScale)) {
129      ctx.fillRect(xAndTime[0], 0, 1, size.height);
130    }
131
132    const localArea = globals.frontendLocalState.selectedArea;
133    const selection = globals.state.currentSelection;
134    if (localArea !== undefined) {
135      const start = Math.min(localArea.startSec, localArea.endSec);
136      const end = Math.max(localArea.startSec, localArea.endSec);
137      this.renderSpan(ctx, size, new TimeSpan(start, end));
138    } else if (selection !== null && selection.kind === 'AREA') {
139      const selectedArea = globals.state.areas[selection.areaId];
140      const start = Math.min(selectedArea.startSec, selectedArea.endSec);
141      const end = Math.max(selectedArea.startSec, selectedArea.endSec);
142      this.renderSpan(ctx, size, new TimeSpan(start, end));
143    }
144
145    if (globals.frontendLocalState.hoveredLogsTimestamp !== -1) {
146      this.renderHover(
147          ctx, size, globals.frontendLocalState.hoveredLogsTimestamp);
148    }
149
150    for (const note of Object.values(globals.state.notes)) {
151      const noteIsSelected = selection !== null && selection.kind === 'AREA' &&
152          selection.noteId === note.id;
153      if (note.noteType === 'AREA' && !noteIsSelected) {
154        const selectedArea = globals.state.areas[note.areaId];
155        this.renderSpan(
156            ctx,
157            size,
158            new TimeSpan(selectedArea.startSec, selectedArea.endSec));
159      }
160    }
161  }
162
163  renderHover(ctx: CanvasRenderingContext2D, size: PanelSize, ts: number) {
164    const timeScale = globals.frontendLocalState.timeScale;
165    const xPos = TRACK_SHELL_WIDTH + Math.floor(timeScale.timeToPx(ts));
166    const offsetTime = timeToString(ts - globals.state.traceTime.startSec);
167    const timeFromStart = timeToString(ts);
168    const label = `${offsetTime} (${timeFromStart})`;
169    drawIBar(ctx, xPos, this.bounds(size), label);
170  }
171
172  renderSpan(ctx: CanvasRenderingContext2D, size: PanelSize, span: TimeSpan) {
173    const timeScale = globals.frontendLocalState.timeScale;
174    const xLeft = timeScale.timeToPx(span.start);
175    const xRight = timeScale.timeToPx(span.end);
176    const label = timeToString(span.duration);
177    drawHBar(
178        ctx,
179        {
180          x: TRACK_SHELL_WIDTH + xLeft,
181          y: 0,
182          width: xRight - xLeft,
183          height: size.height
184        },
185        this.bounds(size),
186        label);
187  }
188
189  private bounds(size: PanelSize): BBox {
190    return {
191      x: TRACK_SHELL_WIDTH,
192      y: 0,
193      width: size.width - TRACK_SHELL_WIDTH,
194      height: size.height
195    };
196  }
197}
198