@samitouri / QOS-React / commits / 87239321b0

Sceduling profiler: Added custom view cursors (#21970)

Brian Vaughn committed Jul 27, 2021 at 12:23 UTC 87239321b0609f7bf1edaaba6aab6be24d6717b1
10 files changed +257 -55
packages/react-devtools-scheduling-profiler/src/CanvasPage.js
+8 -2
@@ -273,10 +273,16 @@ function AutoSizedCanvas({data, height, width}: AutoSizedCanvasProps) {
273 }, [width, height]);
274
275 const interactor = useCallback(interaction => {
276 - if (canvasRef.current === null) {
276 + const canvas = canvasRef.current;
277 + if (canvas === null) {
278 return;
279 }
279 - surfaceRef.current.handleInteraction(interaction);
280 +
281 + const surface = surfaceRef.current;
282 + surface.handleInteraction(interaction);
283 +
284 + canvas.style.cursor = surface.getCurrentCursor() || 'default';
285 +
286 // Defer drawing to canvas until React's commit phase, to avoid drawing
287 // twice and to ensure that both the canvas and DOM elements managed by
288 // React are in sync.
packages/react-devtools-scheduling-profiler/src/content-views/FlamechartView.js
+22 -4
@@ -12,7 +12,13 @@ import type {
12 FlamechartStackFrame,
13 FlamechartStackLayer,
14 } from '../types';
15 -import type {Interaction, MouseMoveInteraction, Rect, Size} from '../view-base';
15 +import type {
16 + Interaction,
17 + MouseMoveInteraction,
18 + Rect,
19 + Size,
20 + ViewRef,
21 +} from '../view-base';
22
23 import {
24 ColorView,
@@ -240,7 +246,11 @@ class FlamechartStackLayerView extends View {
246 /**
247 * @private
248 */
243 - _handleMouseMove(interaction: MouseMoveInteraction) {
249 + _handleMouseMove(
250 + interaction: MouseMoveInteraction,
251 + activeViewRef: ViewRef,
252 + hoveredViewRef: ViewRef,
253 + ) {
254 const {_stackLayer, frame, _intrinsicSize, _onHover, visibleArea} = this;
255 const {location} = interaction.payload;
256 if (!_onHover || !rectContainsPoint(location, visibleArea)) {
@@ -259,6 +269,8 @@ class FlamechartStackLayerView extends View {
269 const width = durationToWidth(duration, scaleFactor);
270 const x = Math.floor(timestampToPosition(timestamp, scaleFactor, frame));
271 if (x <= location.x && x + width >= location.x) {
272 + this.currentCursor = 'pointer';
273 + hoveredViewRef.current = this;
274 _onHover(flamechartStackFrame);
275 return;
276 }
@@ -273,10 +285,16 @@ class FlamechartStackLayerView extends View {
285 _onHover(null);
286 }
287
276 - handleInteraction(interaction: Interaction) {
288 + _didGrab: boolean = false;
289 +
290 + handleInteraction(
291 + interaction: Interaction,
292 + activeViewRef: ViewRef,
293 + hoveredViewRef: ViewRef,
294 + ) {
295 switch (interaction.type) {
296 case 'mousemove':
279 - this._handleMouseMove(interaction);
297 + this._handleMouseMove(interaction, activeViewRef, hoveredViewRef);
298 break;
299 }
300 }
packages/react-devtools-scheduling-profiler/src/content-views/NativeEventsView.js
+22 -5
@@ -8,7 +8,13 @@
8 */
9
10 import type {NativeEvent, ReactProfilerData} from '../types';
11 -import type {Interaction, MouseMoveInteraction, Rect, Size} from '../view-base';
11 +import type {
12 + Interaction,
13 + MouseMoveInteraction,
14 + Rect,
15 + Size,
16 + ViewRef,
17 +} from '../view-base';
18
19 import {
20 durationToWidth,
@@ -72,7 +78,6 @@ export class NativeEventsView extends View {
78 this._profilerData = profilerData;
79
80 this._performPreflightComputations();
75 - console.log(this._depthToNativeEvent);
81 }
82
83 _performPreflightComputations() {
@@ -250,7 +255,11 @@ export class NativeEventsView extends View {
255 /**
256 * @private
257 */
253 - _handleMouseMove(interaction: MouseMoveInteraction) {
258 + _handleMouseMove(
259 + interaction: MouseMoveInteraction,
260 + activeViewRef: ViewRef,
261 + hoveredViewRef: ViewRef,
262 + ) {
263 const {frame, _intrinsicSize, onHover, visibleArea} = this;
264 if (!onHover) {
265 return;
@@ -279,6 +288,10 @@ export class NativeEventsView extends View {
288 hoverTimestamp >= timestamp &&
289 hoverTimestamp <= timestamp + duration
290 ) {
291 + this.currentCursor = 'pointer';
292 +
293 + hoveredViewRef.current = this;
294 +
295 onHover(nativeEvent);
296 return;
297 }
@@ -288,10 +301,14 @@ export class NativeEventsView extends View {
301 onHover(null);
302 }
303
291 - handleInteraction(interaction: Interaction) {
304 + handleInteraction(
305 + interaction: Interaction,
306 + activeViewRef: ViewRef,
307 + hoveredViewRef: ViewRef,
308 + ) {
309 switch (interaction.type) {
310 case 'mousemove':
294 - this._handleMouseMove(interaction);
311 + this._handleMouseMove(interaction, activeViewRef, hoveredViewRef);
312 break;
313 }
314 }
packages/react-devtools-scheduling-profiler/src/content-views/ReactEventsView.js
+20 -4
@@ -8,7 +8,13 @@
8 */
9
10 import type {ReactEvent, ReactProfilerData} from '../types';
11 -import type {Interaction, MouseMoveInteraction, Rect, Size} from '../view-base';
11 +import type {
12 + Interaction,
13 + MouseMoveInteraction,
14 + Rect,
15 + Size,
16 + ViewRef,
17 +} from '../view-base';
18
19 import {
20 positioningScaleFactor,
@@ -225,7 +231,11 @@ export class ReactEventsView extends View {
231 /**
232 * @private
233 */
228 - _handleMouseMove(interaction: MouseMoveInteraction) {
234 + _handleMouseMove(
235 + interaction: MouseMoveInteraction,
236 + activeViewRef: ViewRef,
237 + hoveredViewRef: ViewRef,
238 + ) {
239 const {frame, onHover, visibleArea} = this;
240 if (!onHover) {
241 return;
@@ -260,6 +270,8 @@ export class ReactEventsView extends View {
270 timestamp - eventTimestampAllowance <= hoverTimestamp &&
271 hoverTimestamp <= timestamp + eventTimestampAllowance
272 ) {
273 + this.currentCursor = 'pointer';
274 + hoveredViewRef.current = this;
275 onHover(event);
276 return;
277 }
@@ -268,10 +280,14 @@ export class ReactEventsView extends View {
280 onHover(null);
281 }
282
271 - handleInteraction(interaction: Interaction) {
283 + handleInteraction(
284 + interaction: Interaction,
285 + activeViewRef: ViewRef,
286 + hoveredViewRef: ViewRef,
287 + ) {
288 switch (interaction.type) {
289 case 'mousemove':
274 - this._handleMouseMove(interaction);
290 + this._handleMouseMove(interaction, activeViewRef, hoveredViewRef);
291 break;
292 }
293 }
packages/react-devtools-scheduling-profiler/src/content-views/ReactMeasuresView.js
+20 -4
@@ -8,7 +8,13 @@
8 */
9
10 import type {ReactLane, ReactMeasure, ReactProfilerData} from '../types';
11 -import type {Interaction, MouseMoveInteraction, Rect, Size} from '../view-base';
11 +import type {
12 + Interaction,
13 + MouseMoveInteraction,
14 + Rect,
15 + Size,
16 + ViewRef,
17 +} from '../view-base';
18
19 import {
20 durationToWidth,
@@ -250,7 +256,11 @@ export class ReactMeasuresView extends View {
256 /**
257 * @private
258 */
253 - _handleMouseMove(interaction: MouseMoveInteraction) {
259 + _handleMouseMove(
260 + interaction: MouseMoveInteraction,
261 + activeViewRef: ViewRef,
262 + hoveredViewRef: ViewRef,
263 + ) {
264 const {
265 frame,
266 _intrinsicSize,
@@ -300,6 +310,8 @@ export class ReactMeasuresView extends View {
310 hoverTimestamp >= timestamp &&
311 hoverTimestamp <= timestamp + duration
312 ) {
313 + this.currentCursor = 'pointer';
314 + hoveredViewRef.current = this;
315 onHover(measure);
316 return;
317 }
@@ -308,10 +320,14 @@ export class ReactMeasuresView extends View {
320 onHover(null);
321 }
322
311 - handleInteraction(interaction: Interaction) {
323 + handleInteraction(
324 + interaction: Interaction,
325 + activeViewRef: ViewRef,
326 + hoveredViewRef: ViewRef,
327 + ) {
328 switch (interaction.type) {
329 case 'mousemove':
314 - this._handleMouseMove(interaction);
330 + this._handleMouseMove(interaction, activeViewRef, hoveredViewRef);
331 break;
332 }
333 }
packages/react-devtools-scheduling-profiler/src/content-views/UserTimingMarksView.js
+20 -4
@@ -8,7 +8,13 @@
8 */
9
10 import type {UserTimingMark} from '../types';
11 -import type {Interaction, MouseMoveInteraction, Rect, Size} from '../view-base';
11 +import type {
12 + Interaction,
13 + MouseMoveInteraction,
14 + Rect,
15 + Size,
16 + ViewRef,
17 +} from '../view-base';
18
19 import {
20 positioningScaleFactor,
@@ -185,7 +191,11 @@ export class UserTimingMarksView extends View {
191 /**
192 * @private
193 */
188 - _handleMouseMove(interaction: MouseMoveInteraction) {
194 + _handleMouseMove(
195 + interaction: MouseMoveInteraction,
196 + activeViewRef: ViewRef,
197 + hoveredViewRef: ViewRef,
198 + ) {
199 const {frame, onHover, visibleArea} = this;
200 if (!onHover) {
201 return;
@@ -218,6 +228,8 @@ export class UserTimingMarksView extends View {
228 timestamp - markTimestampAllowance <= hoverTimestamp &&
229 hoverTimestamp <= timestamp + markTimestampAllowance
230 ) {
231 + this.currentCursor = 'pointer';
232 + hoveredViewRef.current = this;
233 onHover(mark);
234 return;
235 }
@@ -226,10 +238,14 @@ export class UserTimingMarksView extends View {
238 onHover(null);
239 }
240
229 - handleInteraction(interaction: Interaction) {
241 + handleInteraction(
242 + interaction: Interaction,
243 + activeViewRef: ViewRef,
244 + hoveredViewRef: ViewRef,
245 + ) {
246 switch (interaction.type) {
247 case 'mousemove':
232 - this._handleMouseMove(interaction);
248 + this._handleMouseMove(interaction, activeViewRef, hoveredViewRef);
249 break;
250 }
251 }
packages/react-devtools-scheduling-profiler/src/view-base/HorizontalPanAndZoomView.js
+46 -7
@@ -19,6 +19,7 @@ import type {
19 } from './useCanvasInteraction';
20 import type {Rect} from './geometry';
21 import type {ScrollState} from './utils/scrollState';
22 +import type {ViewRef} from './Surface';
23
24 import {Surface} from './Surface';
25 import {View} from './View';
@@ -155,13 +156,39 @@ export class HorizontalPanAndZoomView extends View {
156 this._setScrollState(newState);
157 }
158
158 - _handleMouseDown(interaction: MouseDownInteraction) {
159 + _handleMouseDown(
160 + interaction: MouseDownInteraction,
161 + activeViewRef: ViewRef,
162 + hoveredViewRef: ViewRef,
163 + ) {
164 if (rectContainsPoint(interaction.payload.location, this.frame)) {
165 this._isPanning = true;
166 +
167 + activeViewRef.current = this;
168 +
169 + this.currentCursor = 'grabbing';
170 }
171 }
172
164 - _handleMouseMove(interaction: MouseMoveInteraction) {
173 + _handleMouseMove(
174 + interaction: MouseMoveInteraction,
175 + activeViewRef: ViewRef,
176 + hoveredViewRef: ViewRef,
177 + ) {
178 + const isHovered = rectContainsPoint(
179 + interaction.payload.location,
180 + this.frame,
181 + );
182 + if (isHovered) {
183 + hoveredViewRef.current = this;
184 + }
185 +
186 + if (activeViewRef.current === this) {
187 + this.currentCursor = 'grabbing';
188 + } else if (isHovered) {
189 + this.currentCursor = 'grab';
190 + }
191 +
192 if (!this._isPanning) {
193 return;
194 }
@@ -173,10 +200,18 @@ export class HorizontalPanAndZoomView extends View {
200 this._setStateAndInformCallbacksIfChanged(newState);
201 }
202
176 - _handleMouseUp(interaction: MouseUpInteraction) {
203 + _handleMouseUp(
204 + interaction: MouseUpInteraction,
205 + activeViewRef: ViewRef,
206 + hoveredViewRef: ViewRef,
207 + ) {
208 if (this._isPanning) {
209 this._isPanning = false;
210 }
211 +
212 + if (activeViewRef.current === this) {
213 + activeViewRef.current = null;
214 + }
215 }
216
217 _handleWheelPlain(interaction: WheelPlainInteraction) {
@@ -238,16 +273,20 @@ export class HorizontalPanAndZoomView extends View {
273 this._setStateAndInformCallbacksIfChanged(newState);
274 }
275
241 - handleInteraction(interaction: Interaction) {
276 + handleInteraction(
277 + interaction: Interaction,
278 + activeViewRef: ViewRef,
279 + hoveredViewRef: ViewRef,
280 + ) {
281 switch (interaction.type) {
282 case 'mousedown':
244 - this._handleMouseDown(interaction);
283 + this._handleMouseDown(interaction, activeViewRef, hoveredViewRef);
284 break;
285 case 'mousemove':
247 - this._handleMouseMove(interaction);
286 + this._handleMouseMove(interaction, activeViewRef, hoveredViewRef);
287 break;
288 case 'mouseup':
250 - this._handleMouseUp(interaction);
289 + this._handleMouseUp(interaction, activeViewRef, hoveredViewRef);
290 break;
291 case 'wheel-plain':
292 this._handleWheelPlain(interaction);
packages/react-devtools-scheduling-profiler/src/view-base/ResizableSplitView.js
+54 -20
@@ -14,6 +14,7 @@ import type {
14 MouseUpInteraction,
15 } from './useCanvasInteraction';
16 import type {Rect, Size} from './geometry';
17 +import type {ViewRef} from './Surface';
18
19 import {COLORS} from '../content-views/constants';
20 import nullthrows from 'nullthrows';
@@ -82,28 +83,49 @@ class ResizeBar extends View {
83 this._updateColor();
84 }
85
85 - _handleMouseDown(interaction: MouseDownInteraction) {
86 + _handleMouseDown(
87 + interaction: MouseDownInteraction,
88 + activeViewRef: ViewRef,
89 + hoveredViewRef: ViewRef,
90 + ) {
91 const cursorInView = rectContainsPoint(
92 interaction.payload.location,
93 this.frame,
94 );
95 if (cursorInView) {
96 this._setInteractionState('dragging');
97 + activeViewRef.current = this;
98 }
99 }
100
95 - _handleMouseMove(interaction: MouseMoveInteraction) {
101 + _handleMouseMove(
102 + interaction: MouseMoveInteraction,
103 + activeViewRef: ViewRef,
104 + hoveredViewRef: ViewRef,
105 + ) {
106 const cursorInView = rectContainsPoint(
107 interaction.payload.location,
108 this.frame,
109 );
110 +
111 + if (cursorInView || activeViewRef.current === this) {
112 + this.currentCursor = 'ns-resize';
113 + }
114 + if (cursorInView) {
115 + hoveredViewRef.current = this;
116 + }
117 +
118 if (this._interactionState === 'dragging') {
119 return;
120 }
121 this._setInteractionState(cursorInView ? 'hovered' : 'normal');
122 }
123
106 - _handleMouseUp(interaction: MouseUpInteraction) {
124 + _handleMouseUp(
125 + interaction: MouseUpInteraction,
126 + activeViewRef: ViewRef,
127 + hoveredViewRef: ViewRef,
128 + ) {
129 const cursorInView = rectContainsPoint(
130 interaction.payload.location,
131 this.frame,
@@ -111,18 +133,26 @@ class ResizeBar extends View {
133 if (this._interactionState === 'dragging') {
134 this._setInteractionState(cursorInView ? 'hovered' : 'normal');
135 }
136 +
137 + if (activeViewRef.current === this) {
138 + activeViewRef.current = null;
139 + }
140 }
141
116 - handleInteraction(interaction: Interaction) {
142 + handleInteraction(
143 + interaction: Interaction,
144 + activeViewRef: ViewRef,
145 + hoveredViewRef: ViewRef,
146 + ) {
147 switch (interaction.type) {
148 case 'mousedown':
119 - this._handleMouseDown(interaction);
149 + this._handleMouseDown(interaction, activeViewRef, hoveredViewRef);
150 return;
151 case 'mousemove':
122 - this._handleMouseMove(interaction);
152 + this._handleMouseMove(interaction, activeViewRef, hoveredViewRef);
153 return;
154 case 'mouseup':
125 - this._handleMouseUp(interaction);
155 + this._handleMouseUp(interaction, activeViewRef, hoveredViewRef);
156 return;
157 }
158 }
@@ -281,18 +311,6 @@ export class ResizableSplitView extends View {
311 }
312
313 _handleMouseMove(interaction: MouseMoveInteraction) {
284 - const cursorLocation = interaction.payload.location;
285 - const resizeBarFrame = this._getResizeBar().frame;
286 -
287 - const canvas = this._canvasRef.current;
288 - if (canvas !== null) {
289 - if (rectContainsPoint(cursorLocation, resizeBarFrame)) {
290 - canvas.style.cursor = 'ns-resize';
291 - } else {
292 - canvas.style.cursor = 'default';
293 - }
294 - }
295 -
314 const {_resizingState} = this;
315 if (_resizingState) {
316 this._resizingState = {
@@ -309,7 +327,23 @@ export class ResizableSplitView extends View {
327 }
328 }
329
312 - handleInteraction(interaction: Interaction) {
330 + _didGrab: boolean = false;
331 +
332 + getCursorActiveSubView(interaction: Interaction): View | null {
333 + const cursorLocation = interaction.payload.location;
334 + const resizeBarFrame = this._getResizeBar().frame;
335 + if (rectContainsPoint(cursorLocation, resizeBarFrame)) {
336 + return this;
337 + } else {
338 + return null;
339 + }
340 + }
341 +
342 + handleInteraction(
343 + interaction: Interaction,
344 + activeViewRef: ViewRef,
345 + hoveredViewRef: ViewRef,
346 + ) {
347 switch (interaction.type) {
348 case 'mousedown':
349 this._handleMouseDown(interaction);
packages/react-devtools-scheduling-profiler/src/view-base/Surface.js
+26 -1
@@ -10,11 +10,14 @@
10 import type {Interaction} from './useCanvasInteraction';
11 import type {Size} from './geometry';
12
13 +import {createRef} from 'react';
14 import memoize from 'memoize-one';
15
16 import {View} from './View';
17 import {zeroPoint} from './geometry';
18
19 +export type ViewRef = {|current: View | null|};
20 +
21 // hidpi canvas: https://www.html5rocks.com/en/tutorials/canvas/hidpi/
22 function configureRetinaCanvas(canvas, height, width) {
23 const dpr: number = window.devicePixelRatio || 1;
@@ -48,9 +51,13 @@ const getCanvasContext = memoize(
51 */
52 export class Surface {
53 rootView: ?View;
54 +
55 _context: ?CanvasRenderingContext2D;
56 _canvasSize: ?Size;
57
58 + _activeViewRef: ViewRef = createRef();
59 + _hoveredViewRef: ViewRef = createRef();
60 +
61 setCanvas(canvas: HTMLCanvasElement, canvasSize: Size) {
62 this._context = getCanvasContext(
63 canvas,
@@ -80,10 +87,28 @@ export class Surface {
87 rootView.displayIfNeeded(_context);
88 }
89
90 + getCurrentCursor(): string | null {
91 + const activeView = this._activeViewRef.current;
92 + if (activeView !== null) {
93 + return activeView.currentCursor;
94 + } else {
95 + const hoveredView = this._hoveredViewRef.current;
96 + if (hoveredView !== null) {
97 + return hoveredView.currentCursor;
98 + }
99 + }
100 +
101 + return null;
102 + }
103 +
104 handleInteraction(interaction: Interaction) {
105 if (!this.rootView) {
106 return;
107 }
87 - this.rootView.handleInteractionAndPropagateToSubviews(interaction);
108 + this.rootView.handleInteractionAndPropagateToSubviews(
109 + interaction,
110 + this._activeViewRef,
111 + this._hoveredViewRef,
112 + );
113 }
114 }
packages/react-devtools-scheduling-profiler/src/view-base/View.js
+19 -4
@@ -10,6 +10,7 @@
10 import type {Interaction} from './useCanvasInteraction';
11 import type {Rect, Size} from './geometry';
12 import type {Layouter} from './layouter';
13 +import type {ViewRef} from './Surface';
14
15 import {Surface} from './Surface';
16 import {
@@ -28,6 +29,8 @@ import {noopLayout, viewsToLayout, collapseLayoutIntoViews} from './layouter';
29 * subclasses.
30 */
31 export class View {
32 + currentCursor: string | null = null;
33 +
34 surface: Surface;
35
36 frame: Rect;
@@ -253,7 +256,11 @@ export class View {
256 // Internal note: Do not call directly! Use
257 // `handleInteractionAndPropagateToSubviews` so that interactions are
258 // propagated to subviews.
256 - handleInteraction(interaction: Interaction) {}
259 + handleInteraction(
260 + interaction: Interaction,
261 + activeViewRef: ViewRef,
262 + hoveredViewRef: ViewRef,
263 + ) {}
264
265 /**
266 * Handle an `interaction` and propagates it to all of this view's
@@ -265,10 +272,18 @@ export class View {
272 * @see handleInteraction
273 * @protected
274 */
268 - handleInteractionAndPropagateToSubviews(interaction: Interaction) {
269 - this.handleInteraction(interaction);
275 + handleInteractionAndPropagateToSubviews(
276 + interaction: Interaction,
277 + activeViewRef: ViewRef,
278 + hoveredViewRef: ViewRef,
279 + ) {
280 + this.handleInteraction(interaction, activeViewRef, hoveredViewRef);
281 this.subviews.forEach(subview =>
271 - subview.handleInteractionAndPropagateToSubviews(interaction),
282 + subview.handleInteractionAndPropagateToSubviews(
283 + interaction,
284 + activeViewRef,
285 + hoveredViewRef,
286 + ),
287 );
288 }
289 }