• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 m from 'mithril';
16
17import {duration, Span, time, Time, TimeSpan} from '../base/time';
18import {timestampFormat, TimestampFormat} from '../core/timestamp_format';
19
20import {
21  BACKGROUND_COLOR,
22  FOREGROUND_COLOR,
23  TRACK_SHELL_WIDTH,
24} from './css_constants';
25import {globals} from './globals';
26import {
27  getMaxMajorTicks,
28  TickGenerator,
29  TickType,
30  timeScaleForVisibleWindow,
31} from './gridline_helper';
32import {PanelSize} from './panel';
33import {Panel} from './panel_container';
34import {renderDuration} from './widgets/duration';
35
36export interface BBox {
37  x: number;
38  y: number;
39  width: number;
40  height: number;
41}
42
43// Draws a vertical line with two horizontal tails at the left and right and
44// a label in the middle. It looks a bit like a stretched H:
45// |--- Label ---|
46// The |target| bounding box determines where to draw the H.
47// The |bounds| bounding box gives the visible region, this is used to adjust
48// the positioning of the label to ensure it is on screen.
49function drawHBar(
50  ctx: CanvasRenderingContext2D,
51  target: BBox,
52  bounds: BBox,
53  label: string,
54) {
55  ctx.fillStyle = FOREGROUND_COLOR;
56
57  const xLeft = Math.floor(target.x);
58  const xRight = Math.floor(target.x + target.width);
59  const yMid = Math.floor(target.height / 2 + target.y);
60  const xWidth = xRight - xLeft;
61
62  // Don't draw in the track shell.
63  ctx.beginPath();
64  ctx.rect(bounds.x, bounds.y, bounds.width, bounds.height);
65  ctx.clip();
66
67  // Draw horizontal bar of the H.
68  ctx.fillRect(xLeft, yMid, xWidth, 1);
69  // Draw left vertical bar of the H.
70  ctx.fillRect(xLeft, target.y, 1, target.height);
71  // Draw right vertical bar of the H.
72  ctx.fillRect(xRight, target.y, 1, target.height);
73
74  const labelWidth = ctx.measureText(label).width;
75
76  // Find a good position for the label:
77  // By default put the label in the middle of the H:
78  let labelXLeft = Math.floor(xWidth / 2 - labelWidth / 2 + xLeft);
79
80  if (
81    labelWidth > target.width ||
82    labelXLeft < bounds.x ||
83    labelXLeft + labelWidth > bounds.x + bounds.width
84  ) {
85    // It won't fit in the middle or would be at least partly out of bounds
86    // so put it either to the left or right:
87    if (xRight > bounds.x + bounds.width) {
88      // If the H extends off the right side of the screen the label
89      // goes on the left of the H.
90      labelXLeft = xLeft - labelWidth - 3;
91    } else {
92      // Otherwise the label goes on the right of the H.
93      labelXLeft = xRight + 3;
94    }
95  }
96
97  ctx.fillStyle = BACKGROUND_COLOR;
98  ctx.fillRect(labelXLeft - 1, 0, labelWidth + 1, target.height);
99
100  ctx.textBaseline = 'middle';
101  ctx.fillStyle = FOREGROUND_COLOR;
102  ctx.font = '10px Roboto Condensed';
103  ctx.fillText(label, labelXLeft, yMid);
104}
105
106function drawIBar(
107  ctx: CanvasRenderingContext2D,
108  xPos: number,
109  bounds: BBox,
110  label: string,
111) {
112  if (xPos < bounds.x) return;
113
114  ctx.fillStyle = FOREGROUND_COLOR;
115  ctx.fillRect(xPos, 0, 1, bounds.width);
116
117  const yMid = Math.floor(bounds.height / 2 + bounds.y);
118  const labelWidth = ctx.measureText(label).width;
119  const padding = 3;
120
121  let xPosLabel;
122  if (xPos + padding + labelWidth > bounds.width) {
123    xPosLabel = xPos - padding;
124    ctx.textAlign = 'right';
125  } else {
126    xPosLabel = xPos + padding;
127    ctx.textAlign = 'left';
128  }
129
130  ctx.fillStyle = BACKGROUND_COLOR;
131  ctx.fillRect(xPosLabel - 1, 0, labelWidth + 2, bounds.height);
132
133  ctx.textBaseline = 'middle';
134  ctx.fillStyle = FOREGROUND_COLOR;
135  ctx.font = '10px Roboto Condensed';
136  ctx.fillText(label, xPosLabel, yMid);
137}
138
139export class TimeSelectionPanel implements Panel {
140  readonly kind = 'panel';
141  readonly selectable = false;
142
143  render(): m.Children {
144    return m('.time-selection-panel');
145  }
146
147  renderCanvas(ctx: CanvasRenderingContext2D, size: PanelSize) {
148    ctx.fillStyle = '#999';
149    ctx.fillRect(TRACK_SHELL_WIDTH - 2, 0, 2, size.height);
150
151    ctx.save();
152    ctx.beginPath();
153    ctx.rect(TRACK_SHELL_WIDTH, 0, size.width - TRACK_SHELL_WIDTH, size.height);
154    ctx.clip();
155
156    const span = globals.timeline.visibleTimeSpan;
157    if (size.width > TRACK_SHELL_WIDTH && span.duration > 0n) {
158      const maxMajorTicks = getMaxMajorTicks(size.width - TRACK_SHELL_WIDTH);
159      const map = timeScaleForVisibleWindow(TRACK_SHELL_WIDTH, size.width);
160
161      const offset = globals.timestampOffset();
162      const tickGen = new TickGenerator(span, maxMajorTicks, offset);
163      for (const {type, time} of tickGen) {
164        const px = Math.floor(map.timeToPx(time));
165        if (type === TickType.MAJOR) {
166          ctx.fillRect(px, 0, 1, size.height);
167        }
168      }
169    }
170
171    const localArea = globals.timeline.selectedArea;
172    const selection = globals.state.selection;
173    if (localArea !== undefined) {
174      const start = Time.min(localArea.start, localArea.end);
175      const end = Time.max(localArea.start, localArea.end);
176      this.renderSpan(ctx, size, new TimeSpan(start, end));
177    } else if (selection.kind === 'area') {
178      const start = Time.min(selection.start, selection.end);
179      const end = Time.max(selection.start, selection.end);
180      this.renderSpan(ctx, size, new TimeSpan(start, end));
181    }
182
183    if (globals.state.hoverCursorTimestamp !== -1n) {
184      this.renderHover(ctx, size, globals.state.hoverCursorTimestamp);
185    }
186
187    for (const note of Object.values(globals.state.notes)) {
188      const noteIsSelected =
189        selection.kind === 'note' && selection.id === note.id;
190      if (note.noteType === 'SPAN' && !noteIsSelected) {
191        this.renderSpan(ctx, size, new TimeSpan(note.start, note.end));
192      }
193    }
194
195    ctx.restore();
196  }
197
198  renderHover(ctx: CanvasRenderingContext2D, size: PanelSize, ts: time) {
199    const {visibleTimeScale} = globals.timeline;
200    const xPos = TRACK_SHELL_WIDTH + Math.floor(visibleTimeScale.timeToPx(ts));
201    const domainTime = globals.toDomainTime(ts);
202    const label = stringifyTimestamp(domainTime);
203    drawIBar(ctx, xPos, this.bounds(size), label);
204  }
205
206  renderSpan(
207    ctx: CanvasRenderingContext2D,
208    size: PanelSize,
209    span: Span<time, duration>,
210  ) {
211    const {visibleTimeScale} = globals.timeline;
212    const xLeft = visibleTimeScale.timeToPx(span.start);
213    const xRight = visibleTimeScale.timeToPx(span.end);
214    const label = renderDuration(span.duration);
215    drawHBar(
216      ctx,
217      {
218        x: TRACK_SHELL_WIDTH + xLeft,
219        y: 0,
220        width: xRight - xLeft,
221        height: size.height,
222      },
223      this.bounds(size),
224      label,
225    );
226  }
227
228  private bounds(size: PanelSize): BBox {
229    return {
230      x: TRACK_SHELL_WIDTH,
231      y: 0,
232      width: size.width - TRACK_SHELL_WIDTH,
233      height: size.height,
234    };
235  }
236}
237
238function stringifyTimestamp(time: time): string {
239  const fmt = timestampFormat();
240  switch (fmt) {
241    case TimestampFormat.UTC:
242    case TimestampFormat.TraceTz:
243    case TimestampFormat.Timecode:
244      const THIN_SPACE = '\u2009';
245      return Time.toTimecode(time).toString(THIN_SPACE);
246    case TimestampFormat.Raw:
247      return time.toString();
248    case TimestampFormat.RawLocale:
249      return time.toLocaleString();
250    case TimestampFormat.Seconds:
251      return Time.formatSeconds(time);
252    default:
253      const z: never = fmt;
254      throw new Error(`Invalid timestamp ${z}`);
255  }
256}
257