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