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