1/*
2 * Copyright (C) 2023 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 {
18  ElementRef,
19  EventEmitter,
20  HostListener,
21  Input,
22  Output,
23  ViewChild,
24} from '@angular/core';
25import {assertDefined} from 'common/assert_utils';
26import {Point} from 'common/geometry_types';
27import {TimeRange} from 'common/time';
28import {ComponentTimestampConverter} from 'common/timestamp_converter';
29import {Trace, TraceEntry} from 'trace/trace';
30import {TracePosition} from 'trace/trace_position';
31import {CanvasDrawer} from './canvas_drawer';
32
33export abstract class AbstractTimelineRowComponent<T extends {}> {
34  abstract selectedEntry: TraceEntry<T> | undefined;
35  abstract trace: Trace<{}> | undefined;
36
37  @Input() color = '#AF5CF7';
38  @Input() isActive = false;
39  @Input() selectionRange: TimeRange | undefined;
40  @Input() timestampConverter: ComponentTimestampConverter | undefined;
41
42  @Output() readonly onScrollEvent = new EventEmitter<WheelEvent>();
43  @Output() readonly onTraceClicked = new EventEmitter<Trace<object>>();
44  @Output() readonly onTracePositionUpdate = new EventEmitter<TracePosition>();
45  @Output() readonly onMouseXRatioUpdate = new EventEmitter<
46    number | undefined
47  >();
48
49  @ViewChild('canvas', {static: false}) canvasRef: ElementRef | undefined;
50  @ViewChild('wrapper', {static: false}) wrapperRef: ElementRef | undefined;
51
52  canvasDrawer = new CanvasDrawer();
53  protected viewInitialized = false;
54  private observer = new ResizeObserver(() => this.initializeCanvas());
55
56  getCanvas(): HTMLCanvasElement {
57    return this.canvasRef?.nativeElement;
58  }
59
60  getBackgroundColor() {
61    return this.isActive ? 'var(--drawer-block-secondary)' : undefined;
62  }
63
64  ngAfterViewInit() {
65    this.observer.observe(assertDefined(this.wrapperRef).nativeElement);
66    this.initializeCanvas();
67  }
68
69  ngOnChanges() {
70    if (this.viewInitialized) {
71      this.redraw();
72    }
73  }
74
75  ngOnDestroy() {
76    this.observer.disconnect();
77  }
78
79  initializeCanvas() {
80    const canvas = this.getCanvas();
81
82    // Reset any size before computing new size to avoid it interfering with size computations
83    canvas.width = 0;
84    canvas.height = 0;
85    canvas.style.width = 'auto';
86    canvas.style.height = 'auto';
87
88    const htmlElement = assertDefined(this.wrapperRef).nativeElement;
89
90    const computedStyle = getComputedStyle(htmlElement);
91    const width = htmlElement.offsetWidth;
92    const height =
93      htmlElement.offsetHeight -
94      // tslint:disable-next-line:ban
95      parseFloat(computedStyle.paddingTop) -
96      // tslint:disable-next-line:ban
97      parseFloat(computedStyle.paddingBottom);
98
99    const HiPPIwidth = window.devicePixelRatio * width;
100    const HiPPIheight = window.devicePixelRatio * height;
101
102    canvas.width = HiPPIwidth;
103    canvas.height = HiPPIheight;
104    canvas.style.width = width + 'px';
105    canvas.style.height = height + 'px';
106
107    // ensure all drawing operations are scaled
108    if (window.devicePixelRatio !== 1) {
109      const context = canvas.getContext('2d')!;
110      context.scale(window.devicePixelRatio, window.devicePixelRatio);
111    }
112
113    this.canvasDrawer.setCanvas(this.getCanvas());
114    this.redraw();
115
116    canvas.addEventListener('mousemove', (event: MouseEvent) => {
117      this.handleMouseMove(event);
118    });
119    canvas.addEventListener('mousedown', (event: MouseEvent) => {
120      this.handleMouseDown(event);
121    });
122    canvas.addEventListener('mouseout', (event: MouseEvent) => {
123      this.handleMouseOut(event);
124    });
125
126    this.viewInitialized = true;
127  }
128
129  async handleMouseDown(e: MouseEvent) {
130    e.preventDefault();
131    e.stopPropagation();
132    const mousePoint = {
133      x: e.offsetX,
134      y: e.offsetY,
135    };
136
137    const transitionEntry = this.getEntryAt(mousePoint);
138    // TODO: This can probably get made better by getting the transition and checking both the end and start timestamps match
139    if (transitionEntry && transitionEntry !== this.selectedEntry) {
140      this.redraw();
141      this.selectedEntry = transitionEntry;
142      this.onTracePositionUpdate.emit(
143        TracePosition.fromTraceEntry(transitionEntry),
144      );
145    } else if (!transitionEntry && this.trace) {
146      this.onTraceClicked.emit(this.trace);
147    }
148  }
149
150  handleMouseMove(e: MouseEvent) {
151    e.preventDefault();
152    e.stopPropagation();
153    const mousePoint = {
154      x: e.offsetX,
155      y: e.offsetY,
156    };
157
158    this.onHover(mousePoint);
159  }
160
161  @HostListener('wheel', ['$event'])
162  updateScroll(event: WheelEvent) {
163    this.onScrollEvent.emit(event);
164  }
165
166  onTimelineClick(event: MouseEvent) {
167    if ((event.target as HTMLElement).id === 'canvas') {
168      return;
169    }
170    this.onTraceClicked.emit(assertDefined(this.trace));
171  }
172
173  trackMousePos(event: MouseEvent) {
174    const canvas = event.target as HTMLCanvasElement;
175    this.onMouseXRatioUpdate.emit(event.offsetX / canvas.offsetWidth);
176  }
177
178  onMouseLeave(event: MouseEvent) {
179    this.onMouseXRatioUpdate.emit(undefined);
180  }
181
182  protected redraw() {
183    this.canvasDrawer.clear();
184    this.drawTimeline();
185  }
186
187  abstract drawTimeline(): void;
188  protected abstract getEntryAt(mousePoint: Point): TraceEntry<T> | undefined;
189  protected abstract onHover(mousePoint: Point): void;
190  protected abstract handleMouseOut(e: MouseEvent): void;
191}
192