1/*
2 * Copyright (C) 2022 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17import {Component, Input} from '@angular/core';
18import {assertDefined} from 'common/assert_utils';
19import {Point} from 'common/geometry_types';
20import {Rect} from 'common/rect';
21import {Timestamp} from 'common/time';
22import {Trace, TraceEntry} from 'trace/trace';
23import {AbstractTimelineRowComponent} from './abstract_timeline_row_component';
24
25@Component({
26  selector: 'single-timeline',
27  template: `
28    <div
29      class="single-timeline"
30      (click)="onTimelineClick($event)"
31      [style.background-color]="getBackgroundColor()" #wrapper>
32      <canvas
33        id="canvas"
34        (mousemove)="trackMousePos($event)"
35        (mouseleave)="onMouseLeave($event)" #canvas></canvas>
36    </div>
37  `,
38  styles: [
39    `
40      .single-timeline {
41        height: 2rem;
42        padding: 1rem 0;
43      }
44      .single-timeline:hover {
45        background-color: var(--hover-element-color);
46        cursor: pointer;
47      }
48    `,
49  ],
50})
51export class DefaultTimelineRowComponent extends AbstractTimelineRowComponent<{}> {
52  @Input() selectedEntry: TraceEntry<{}> | undefined;
53  @Input() trace: Trace<{}> | undefined;
54
55  hoveringEntry?: Timestamp;
56
57  ngOnInit() {
58    assertDefined(this.trace);
59    assertDefined(this.selectionRange);
60  }
61
62  getEntryWidth() {
63    return this.canvasDrawer.getScaledCanvasHeight();
64  }
65
66  getAvailableWidth() {
67    return Math.floor(
68      this.canvasDrawer.getScaledCanvasWidth() - this.getEntryWidth(),
69    );
70  }
71
72  override onHover(mousePoint: Point) {
73    this.drawEntryHover(mousePoint);
74  }
75
76  override handleMouseOut(e: MouseEvent) {
77    if (this.hoveringEntry) {
78      // If undefined there is no current hover effect so no need to clear
79      this.redraw();
80    }
81    this.hoveringEntry = undefined;
82  }
83
84  override drawTimeline() {
85    assertDefined(this.trace)
86      .sliceTime(
87        assertDefined(this.selectionRange).from,
88        assertDefined(this.selectionRange).to.add(1n),
89      )
90      .forEachTimestamp((entry) => {
91        this.drawEntry(entry);
92      });
93    this.drawSelectedEntry();
94  }
95
96  protected override getEntryAt(mousePoint: Point): TraceEntry<{}> | undefined {
97    const timestampOfClick = this.getTimestampOf(mousePoint.x);
98    const candidateEntry = assertDefined(this.trace).findLastLowerOrEqualEntry(
99      timestampOfClick,
100    );
101
102    if (candidateEntry !== undefined) {
103      const timestamp = candidateEntry.getTimestamp();
104      const rect = this.entryRect(timestamp);
105      if (rect.containsPoint(mousePoint)) {
106        return candidateEntry;
107      }
108    }
109
110    return undefined;
111  }
112
113  private drawEntryHover(mousePoint: Point) {
114    const currentHoverEntry = this.getEntryAt(mousePoint)?.getTimestamp();
115
116    if (this.hoveringEntry === currentHoverEntry) {
117      return;
118    }
119
120    if (this.hoveringEntry) {
121      // If null there is no current hover effect so no need to clear
122      this.redraw();
123    }
124
125    this.hoveringEntry = currentHoverEntry;
126
127    if (!this.hoveringEntry) {
128      return;
129    }
130
131    const rect = this.entryRect(this.hoveringEntry);
132
133    this.canvasDrawer.drawRect(rect, this.color, 1.0);
134    this.canvasDrawer.drawRectBorder(rect);
135  }
136
137  private entryRect(entry: Timestamp, padding = 0): Rect {
138    const xPos = this.getXPosOf(entry);
139
140    return new Rect(
141      xPos + padding,
142      padding,
143      this.getEntryWidth() - 2 * padding,
144      this.getEntryWidth() - 2 * padding,
145    );
146  }
147
148  private getXPosOf(entry: Timestamp): number {
149    const start = assertDefined(this.selectionRange).from.getValueNs();
150    const end = assertDefined(this.selectionRange).to.getValueNs();
151
152    return Number(
153      (BigInt(this.getAvailableWidth()) * (entry.getValueNs() - start)) /
154        (end - start),
155    );
156  }
157
158  private getTimestampOf(x: number): Timestamp {
159    const start = assertDefined(this.selectionRange).from.getValueNs();
160    const end = assertDefined(this.selectionRange).to.getValueNs();
161    const ts =
162      (BigInt(Math.floor(x)) * (end - start)) /
163        BigInt(this.getAvailableWidth()) +
164      start;
165    return assertDefined(this.timestampConverter).makeTimestampFromNs(ts);
166  }
167
168  private drawEntry(entry: Timestamp) {
169    const rect = this.entryRect(entry);
170
171    this.canvasDrawer.drawRect(rect, this.color, 0.2);
172  }
173
174  private drawSelectedEntry() {
175    if (this.selectedEntry === undefined) {
176      return;
177    }
178
179    const rect = this.entryRect(this.selectedEntry.getTimestamp(), 1);
180    this.canvasDrawer.drawRect(rect, this.color, 1.0);
181    this.canvasDrawer.drawRectBorder(rect);
182  }
183}
184