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 {assertDefined} from 'common/assert_utils'; 18import {Point} from 'common/geometry_types'; 19import {Trace} from 'trace/trace'; 20import { 21 CanvasMouseHandler, 22 DragListener, 23 DropListener, 24} from './canvas_mouse_handler'; 25import {DraggableCanvasObject} from './draggable_canvas_object'; 26import {MiniTimelineDrawer} from './mini_timeline_drawer'; 27 28/** 29 * Canvas mouse handling implementation 30 * @docs-private 31 */ 32export class CanvasMouseHandlerImpl implements CanvasMouseHandler { 33 // Ordered top most element to bottom most 34 private draggableObjects: DraggableCanvasObject[] = []; 35 private draggingObject: DraggableCanvasObject | undefined = undefined; 36 37 private onDrag = new Map<DraggableCanvasObject, DragListener>(); 38 private onDrop = new Map<DraggableCanvasObject, DropListener>(); 39 40 constructor( 41 private drawer: MiniTimelineDrawer, 42 private defaultCursor = 'auto', 43 private onUnhandledMouseDown: ( 44 point: Point, 45 button: number, 46 trace: Trace<object> | undefined, 47 ) => void = (point, button) => {}, 48 ) { 49 this.drawer.canvas.addEventListener('mousemove', (event) => { 50 this.handleMouseMove(event); 51 }); 52 this.drawer.canvas.addEventListener('mousedown', async (event) => { 53 await this.handleMouseDown(event); 54 }); 55 this.drawer.canvas.addEventListener('mouseup', (event) => { 56 this.handleMouseUp(event); 57 }); 58 this.drawer.canvas.addEventListener('mouseout', (event) => { 59 this.handleMouseOut(event); 60 }); 61 } 62 63 registerDraggableObject( 64 draggableObject: DraggableCanvasObject, 65 onDrag: DragListener, 66 onDrop: DropListener, 67 ) { 68 this.onDrag.set(draggableObject, onDrag); 69 this.onDrop.set(draggableObject, onDrop); 70 } 71 72 notifyDrawnOnTop(draggableObject: DraggableCanvasObject) { 73 const foundIndex = this.draggableObjects.indexOf(draggableObject); 74 if (foundIndex !== -1) { 75 this.draggableObjects.splice(foundIndex, 1); 76 } 77 this.draggableObjects.unshift(draggableObject); 78 } 79 80 private async handleMouseDown(e: MouseEvent) { 81 e.preventDefault(); 82 e.stopPropagation(); 83 const mousePoint = this.getPos(e); 84 85 const clickedObject = this.objectAt(mousePoint); 86 if (clickedObject !== undefined) { 87 this.draggingObject = clickedObject; 88 } else { 89 const trace = await this.drawer.getTraceClicked(mousePoint); 90 this.onUnhandledMouseDown(mousePoint, e.button, trace); 91 } 92 this.updateCursor(mousePoint); 93 } 94 95 private handleMouseMove(e: MouseEvent) { 96 e.preventDefault(); 97 e.stopPropagation(); 98 const mousePoint = this.getPos(e); 99 100 if (this.draggingObject !== undefined) { 101 const onDragCallback = this.onDrag.get(this.draggingObject); 102 if (onDragCallback !== undefined) { 103 onDragCallback(mousePoint.x, mousePoint.y); 104 } 105 } else { 106 this.drawer.updateHover(mousePoint); 107 } 108 109 this.updateCursor(mousePoint); 110 } 111 112 private handleMouseOut(e: MouseEvent) { 113 this.drawer.updateHover(undefined); 114 this.handleMouseUp(e); 115 } 116 117 private handleMouseUp(e: MouseEvent) { 118 e.preventDefault(); 119 e.stopPropagation(); 120 const mousePoint = this.getPos(e); 121 122 if (this.draggingObject !== undefined) { 123 const onDropCallback = this.onDrop.get(this.draggingObject); 124 if (onDropCallback !== undefined) { 125 onDropCallback(mousePoint.x, mousePoint.y); 126 } 127 } 128 129 this.draggingObject = undefined; 130 this.updateCursor(mousePoint); 131 } 132 133 private getPos(e: MouseEvent): Point { 134 let mouseX = e.offsetX; 135 const mouseY = e.offsetY; 136 const padding = this.drawer.getPadding(); 137 const width = this.drawer.getWidth(); 138 139 if (mouseX < padding.left) { 140 mouseX = padding.left; 141 } 142 143 if (mouseX > width - padding.right) { 144 mouseX = width - padding.right; 145 } 146 147 return {x: mouseX, y: mouseY}; 148 } 149 150 private updateCursor(mousePoint: Point) { 151 const hoverObject = this.objectAt(mousePoint); 152 if (hoverObject !== undefined) { 153 if (hoverObject === this.draggingObject) { 154 this.drawer.canvas.style.cursor = 'grabbing'; 155 } else { 156 this.drawer.canvas.style.cursor = 'grab'; 157 } 158 } else { 159 this.drawer.canvas.style.cursor = this.defaultCursor; 160 } 161 } 162 163 private objectAt(mousePoint: Point): DraggableCanvasObject | undefined { 164 for (const object of this.draggableObjects) { 165 const ctx = assertDefined(this.drawer.canvas.getContext('2d')); 166 object.definePath(ctx); 167 if (ctx.isPointInPath(mousePoint.x, mousePoint.y)) { 168 return object; 169 } 170 } 171 172 return undefined; 173 } 174} 175