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