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
}