DevTools: Scheduling profiler: Add vertical scroll bar (#22005)
Co-authored-by: E-Liang Tan <eliang@eliangtan.com>
Brian Vaughn committed
Aug 11, 2021 at 14:19 UTC
280e3f936f89583b44e10f29ed0ed0155b50ab8a
14 files changed
+731
-284
packages/react-devtools-scheduling-profiler/src/CanvasPage.js
+21
-10
@@ -30,14 +30,12 @@ import {copy} from 'clipboard-js';
30
import prettyMilliseconds from 'pretty-ms';
31
32
import {
33
- BackgroundColorView,
33
HorizontalPanAndZoomView,
34
ResizableView,
35
+ VerticalScrollOverflowView,
36
Surface,
37
VerticalScrollView,
38
View,
39
- createComposedLayout,
40
- lastViewTakesUpRemainingSpaceLayout,
39
useCanvasInteraction,
40
verticallyStackedLayout,
41
zeroPoint,
@@ -325,10 +323,9 @@ function AutoSizedCanvas({
323
const rootView = new View(
324
surface,
325
defaultFrame,
328
- createComposedLayout(
329
- verticallyStackedLayout,
330
- lastViewTakesUpRemainingSpaceLayout,
331
- ),
326
+ verticallyStackedLayout,
327
+ defaultFrame,
328
+ COLORS.BACKGROUND,
329
);
330
rootView.addSubview(axisMarkersViewWrapper);
331
if (userTimingMarksViewWrapper !== null) {
@@ -345,10 +342,14 @@ function AutoSizedCanvas({
342
}
343
rootView.addSubview(flamechartViewWrapper);
344
348
- // If subviews are less than the available height, fill remaining height with a solid color.
349
- rootView.addSubview(new BackgroundColorView(surface, defaultFrame));
345
+ const verticalScrollOverflowView = new VerticalScrollOverflowView(
346
+ surface,
347
+ defaultFrame,
348
+ rootView,
349
+ viewState,
350
+ );
351
351
- surfaceRef.current.rootView = rootView;
352
+ surfaceRef.current.rootView = verticalScrollOverflowView;
353
}, [data]);
354
355
useLayoutEffect(() => {
@@ -401,6 +402,16 @@ function AutoSizedCanvas({
402
const surface = surfaceRef.current;
403
surface.handleInteraction(interaction);
404
405
+ // Flush any display work that got queued up as part of the previous interaction.
406
+ // Typically there should be no work, but certain interactions may need a second pass.
407
+ // For example, the ResizableView may collapse/expand its contents,
408
+ // which requires a second layout pass for an ancestor VerticalScrollOverflowView.
409
+ //
410
+ // TODO It would be nice to remove this call for performance reasons.
411
+ // To do that, we'll need to address the UX bug with VerticalScrollOverflowView.
412
+ // For more info see: https://github.com/facebook/react/pull/22005#issuecomment-896953399
413
+ surface.displayIfNeeded();
414
+
415
canvas.style.cursor = surface.getCurrentCursor() || 'default';
416
417
// Defer drawing to canvas until React's commit phase, to avoid drawing
packages/react-devtools-scheduling-profiler/src/import-worker/preprocessData.js
+4
-2
@@ -241,9 +241,11 @@ function processTimelineEvent(
241
242
// Reduce noise from events like DOMActivate, load/unload, etc. which are usually not relevant
243
if (
244
- type.startsWith('blur') ||
245
- type.startsWith('click') ||
244
+ type === 'blur' ||
245
+ type === 'click' ||
246
+ type === 'input' ||
247
type.startsWith('focus') ||
248
+ type.startsWith('key') ||
249
type.startsWith('mouse') ||
250
type.startsWith('pointer')
251
) {
packages/react-devtools-scheduling-profiler/src/view-base/VerticalScrollOverflowView.js
deleted
-3
@@ -1,3 +0,0 @@
1
-// TODO Vertically stack views (via verticallyStackedLayout).
2
-// If stacked views are taller than the available height, a vertical scrollbar will be shown on the side,
3
-// and width will be adjusted to subtract the width of the scrollbar.
packages/react-devtools-scheduling-profiler/src/view-base/VerticalScrollView.js
+52
-24
@@ -34,10 +34,16 @@ const CARET_MARGIN = 3;
34
const CARET_WIDTH = 5;
35
const CARET_HEIGHT = 3;
36
37
+type OnChangeCallback = (
38
+ scrollState: ScrollState,
39
+ containerLength: number,
40
+) => void;
41
+
42
export class VerticalScrollView extends View {
43
_contentView: View;
44
_isPanning: boolean;
45
_mutableViewStateKey: string;
46
+ _onChangeCallback: OnChangeCallback | null;
47
_scrollState: ScrollState;
48
_viewState: ViewState;
49
@@ -53,6 +59,7 @@ export class VerticalScrollView extends View {
59
this._contentView = contentView;
60
this._isPanning = false;
61
this._mutableViewStateKey = label + ':VerticalScrollView';
62
+ this._onChangeCallback = null;
63
this._scrollState = {
64
offset: 0,
65
length: 0,
@@ -146,26 +153,45 @@ export class VerticalScrollView extends View {
153
super.layoutSubviews();
154
}
155
149
- handleInteraction(interaction: Interaction) {
156
+ handleInteraction(interaction: Interaction): ?boolean {
157
switch (interaction.type) {
158
case 'mousedown':
152
- this._handleMouseDown(interaction);
153
- break;
159
+ return this._handleMouseDown(interaction);
160
case 'mousemove':
155
- this._handleMouseMove(interaction);
156
- break;
161
+ return this._handleMouseMove(interaction);
162
case 'mouseup':
158
- this._handleMouseUp(interaction);
159
- break;
163
+ return this._handleMouseUp(interaction);
164
case 'wheel-shift':
161
- this._handleWheelShift(interaction);
162
- break;
165
+ return this._handleWheelShift(interaction);
166
}
167
}
168
169
+ onChange(callback: OnChangeCallback) {
170
+ this._onChangeCallback = callback;
171
+ }
172
+
173
+ scrollBy(deltaY: number): boolean {
174
+ const newState = translateState({
175
+ state: this._scrollState,
176
+ delta: -deltaY,
177
+ containerLength: this.frame.size.height,
178
+ });
179
+
180
+ // If the state is updated by this wheel scroll,
181
+ // return true to prevent the interaction from bubbling.
182
+ // For instance, this prevents the outermost container from also scrolling.
183
+ return this._setScrollState(newState);
184
+ }
185
+
186
_handleMouseDown(interaction: MouseDownInteraction) {
187
if (rectContainsPoint(interaction.payload.location, this.frame)) {
168
- this._isPanning = true;
188
+ const frameHeight = this.frame.size.height;
189
+ const contentHeight = this._contentView.desiredSize().height;
190
+ // Don't claim drag operations if the content is not tall enough to be scrollable.
191
+ // This would block any outer scroll views from working.
192
+ if (frameHeight < contentHeight) {
193
+ this._isPanning = true;
194
+ }
195
}
196
}
197
@@ -179,6 +205,7 @@ export class VerticalScrollView extends View {
205
containerLength: this.frame.size.height,
206
});
207
this._setScrollState(newState);
208
+ return true;
209
}
210
211
_handleMouseUp(interaction: MouseUpInteraction) {
@@ -187,31 +214,27 @@ export class VerticalScrollView extends View {
214
}
215
}
216
190
- _handleWheelShift(interaction: WheelWithShiftInteraction) {
217
+ _handleWheelShift(interaction: WheelWithShiftInteraction): boolean {
218
const {
219
location,
220
delta: {deltaX, deltaY},
221
} = interaction.payload;
222
+
223
if (!rectContainsPoint(location, this.frame)) {
196
- return; // Not scrolling on view
224
+ return false; // Not scrolling on view
225
}
226
227
const absDeltaX = Math.abs(deltaX);
228
const absDeltaY = Math.abs(deltaY);
229
if (absDeltaX > absDeltaY) {
202
- return; // Scrolling horizontally
230
+ return false; // Scrolling horizontally
231
}
232
233
if (absDeltaY < MOVE_WHEEL_DELTA_THRESHOLD) {
206
- return;
234
+ return false; // Movement was too small and should be ignored.
235
}
236
209
- const newState = translateState({
210
- state: this._scrollState,
211
- delta: -deltaY,
212
- containerLength: this.frame.size.height,
213
- });
214
- this._setScrollState(newState);
237
+ return this.scrollBy(deltaY);
238
}
239
240
_restoreMutableViewState() {
@@ -231,10 +254,7 @@ export class VerticalScrollView extends View {
254
this.setNeedsDisplay();
255
}
256
234
- /**
235
- * @private
236
- */
237
- _setScrollState(proposedState: ScrollState) {
257
+ _setScrollState(proposedState: ScrollState): boolean {
258
const height = this._contentView.frame.size.height;
259
const clampedState = clampState({
260
state: proposedState,
@@ -247,6 +267,14 @@ export class VerticalScrollView extends View {
267
this._scrollState.length = clampedState.length;
268
269
this.setNeedsDisplay();
270
+
271
+ if (this._onChangeCallback !== null) {
272
+ this._onChangeCallback(clampedState, this.frame.size.height);
273
+ }
274
+
275
+ return true;
276
}
277
+
278
+ return false;
279
}
280
}
packages/react-devtools-scheduling-profiler/src/view-base/View.js
+46
-8
@@ -29,6 +29,8 @@ import {noopLayout, viewsToLayout, collapseLayoutIntoViews} from './layouter';
29
* subclasses.
30
*/
31
export class View {
32
+ _backgroundColor: string | null;
33
+
34
currentCursor: string | null = null;
35
36
surface: Surface;
@@ -70,7 +72,9 @@ export class View {
72
frame: Rect,
73
layouter: Layouter = noopLayout,
74
visibleArea: Rect = frame,
75
+ backgroundColor?: string | null = null,
76
) {
77
+ this._backgroundColor = backgroundColor || null;
78
this.surface = surface;
79
this.frame = frame;
80
this._layouter = layouter;
@@ -246,6 +250,20 @@ export class View {
250
subview.displayIfNeeded(context, viewRefs);
251
}
252
});
253
+
254
+ const backgroundColor = this._backgroundColor;
255
+ if (backgroundColor !== null) {
256
+ const desiredSize = this.desiredSize();
257
+ if (visibleArea.size.height > desiredSize.height) {
258
+ context.fillStyle = backgroundColor;
259
+ context.fillRect(
260
+ visibleArea.origin.x,
261
+ visibleArea.origin.y + desiredSize.height,
262
+ visibleArea.size.width,
263
+ visibleArea.size.height - desiredSize.height,
264
+ );
265
+ }
266
+ }
267
}
268
269
/**
@@ -255,7 +273,7 @@ export class View {
273
*
274
* NOTE: Do not call directly! Use `handleInteractionAndPropagateToSubviews`
275
*/
258
- handleInteraction(interaction: Interaction, viewRefs: ViewRefs) {}
276
+ handleInteraction(interaction: Interaction, viewRefs: ViewRefs): ?boolean {}
277
278
/**
279
* Handle an `interaction` and propagates it to all of this view's
@@ -270,19 +288,39 @@ export class View {
288
handleInteractionAndPropagateToSubviews(
289
interaction: Interaction,
290
viewRefs: ViewRefs,
273
- ) {
291
+ ): boolean {
292
const {subviews, visibleArea} = this;
293
294
if (visibleArea.size.height === 0) {
277
- return;
295
+ return false;
296
}
297
280
- this.handleInteraction(interaction, viewRefs);
281
-
282
- subviews.forEach(subview => {
298
+ // Pass the interaction to subviews first,
299
+ // so they have the opportunity to claim it before it bubbles.
300
+ //
301
+ // Views are painted first to last,
302
+ // so they should process interactions last to first,
303
+ // so views in front (on top) can claim the interaction first.
304
+ for (let i = subviews.length - 1; i >= 0; i--) {
305
+ const subview = subviews[i];
306
if (rectIntersectsRect(visibleArea, subview.visibleArea)) {
284
- subview.handleInteractionAndPropagateToSubviews(interaction, viewRefs);
307
+ const didSubviewHandle =
308
+ subview.handleInteractionAndPropagateToSubviews(
309
+ interaction,
310
+ viewRefs,
311
+ ) === true;
312
+ if (didSubviewHandle) {
313
+ return true;
314
+ }
315
}
286
- });
316
+ }
317
+
318
+ const didSelfHandle =
319
+ this.handleInteraction(interaction, viewRefs) === true;
320
+ if (didSelfHandle) {
321
+ return true;
322
+ }
323
+
324
+ return false;
325
}
326
}
packages/react-devtools-scheduling-profiler/src/view-base/index.js
+2
-1
@@ -9,10 +9,11 @@
9
10
export * from './BackgroundColorView';
11
export * from './HorizontalPanAndZoomView';
12
-export * from './ResizableView';
12
export * from './Surface';
13
export * from './VerticalScrollView';
14
export * from './View';
15
export * from './geometry';
16
export * from './layouter';
17
+export * from './resizable';
18
export * from './useCanvasInteraction';
19
+export * from './vertical-scroll-overflow';
packages/react-devtools-scheduling-profiler/src/view-base/layouter.js
-44
@@ -206,50 +206,6 @@ export const atLeastContainerHeightLayout: Layouter = (
206
}));
207
};
208
209
-/**
210
- * Forces last view to take up the space below the second-last view.
211
- * Intended to be used with a vertical stack layout.
212
- */
213
-export const lastViewTakesUpRemainingSpaceLayout: Layouter = (
214
- layout,
215
- containerFrame,
216
-) => {
217
- if (layout.length === 0) {
218
- // Nothing to do
219
- return layout;
220
- }
221
-
222
- if (layout.length === 1) {
223
- // No second-last view; the view should just take up the container height
224
- return containerHeightLayout(layout, containerFrame);
225
- }
226
-
227
- const layoutInfoToPassThrough = layout.slice(0, layout.length - 1);
228
- const secondLastLayoutInfo =
229
- layoutInfoToPassThrough[layoutInfoToPassThrough.length - 1];
230
-
231
- const remainingHeight =
232
- containerFrame.size.height -
233
- secondLastLayoutInfo.frame.origin.y -
234
- secondLastLayoutInfo.frame.size.height;
235
- const height = Math.max(remainingHeight, 0); // Prevent negative heights
236
-
237
- const lastLayoutInfo = layout[layout.length - 1];
238
- return [
239
- ...layoutInfoToPassThrough,
240
- {
241
- ...lastLayoutInfo,
242
- frame: {
243
- origin: lastLayoutInfo.frame.origin,
244
- size: {
245
- width: lastLayoutInfo.frame.size.width,
246
- height,
247
- },
248
- },
249
- },
250
- ];
251
-};
252
-
209
/**
210
* Create a layouter that applies each layouter in `layouters` in sequence.
211
*/
packages/react-devtools-scheduling-profiler/src/view-base/resizable/ResizableView.js
renamed
+26
-192
@@ -14,20 +14,17 @@ import type {
14
MouseDownInteraction,
15
MouseMoveInteraction,
16
MouseUpInteraction,
17
-} from './useCanvasInteraction';
18
-import type {Rect} from './geometry';
19
-import type {ViewRefs} from './Surface';
20
-import type {ViewState} from '../types';
21
-
22
-import {BORDER_SIZE, COLORS} from '../content-views/constants';
23
-import {drawText} from '../content-views/utils/text';
24
-import {Surface} from './Surface';
25
-import {View} from './View';
26
-import {intersectionOfRects, rectContainsPoint} from './geometry';
27
-import {noopLayout} from './layouter';
28
-import {clamp} from './utils/clamp';
29
-
30
-type ResizeBarState = 'normal' | 'hovered' | 'dragging';
17
+} from '../useCanvasInteraction';
18
+import type {Rect} from '../geometry';
19
+import type {ViewRefs} from '../Surface';
20
+import type {ViewState} from '../../types';
21
+
22
+import {ResizeBarView} from './ResizeBarView';
23
+import {Surface} from '../Surface';
24
+import {View} from '../View';
25
+import {rectContainsPoint} from '../geometry';
26
+import {noopLayout} from '../layouter';
27
+import {clamp} from '../utils/clamp';
28
29
type ResizingState = $ReadOnly<{|
30
/** Distance between top of resize bar and mouseY */
@@ -41,8 +38,6 @@ type LayoutState = {|
38
barOffsetY: number,
39
|};
40
44
-const RESIZE_BAR_DOT_RADIUS = 1;
45
-const RESIZE_BAR_DOT_SPACING = 4;
41
const RESIZE_BAR_HEIGHT = 8;
42
const RESIZE_BAR_WITH_LABEL_HEIGHT = 16;
43
@@ -51,175 +46,11 @@ const HIDDEN_RECT = {
46
size: {width: 0, height: 0},
47
};
48
54
-class ResizeBar extends View {
55
- _interactionState: ResizeBarState = 'normal';
56
- _label: string;
57
-
58
- showLabel: boolean = false;
59
-
60
- constructor(surface: Surface, frame: Rect, label: string) {
61
- super(surface, frame, noopLayout);
62
-
63
- this._label = label;
64
- }
65
-
66
- desiredSize() {
67
- return this.showLabel
68
- ? {height: RESIZE_BAR_WITH_LABEL_HEIGHT, width: 0}
69
- : {height: RESIZE_BAR_HEIGHT, width: 0};
70
- }
71
-
72
- draw(context: CanvasRenderingContext2D, viewRefs: ViewRefs) {
73
- const {visibleArea} = this;
74
- const {x, y} = visibleArea.origin;
75
- const {width, height} = visibleArea.size;
76
-
77
- const isActive =
78
- this._interactionState === 'dragging' ||
79
- (this._interactionState === 'hovered' && viewRefs.activeView === null);
80
-
81
- context.fillStyle = isActive
82
- ? COLORS.REACT_RESIZE_BAR_ACTIVE
83
- : COLORS.REACT_RESIZE_BAR;
84
- context.fillRect(x, y, width, height);
85
-
86
- context.fillStyle = COLORS.REACT_RESIZE_BAR_BORDER;
87
- context.fillRect(x, y, width, BORDER_SIZE);
88
- context.fillRect(x, y + height - BORDER_SIZE, width, BORDER_SIZE);
89
-
90
- const horizontalCenter = x + width / 2;
91
- const verticalCenter = y + height / 2;
92
-
93
- if (this.showLabel) {
94
- // When the resize view is collapsed entirely,
95
- // rather than showing a resize bar– this view displays a label.
96
- const labelRect: Rect = {
97
- origin: {
98
- x: 0,
99
- y: y + height - RESIZE_BAR_WITH_LABEL_HEIGHT,
100
- },
101
- size: {
102
- width: visibleArea.size.width,
103
- height: visibleArea.size.height,
104
- },
105
- };
106
-
107
- const drawableRect = intersectionOfRects(labelRect, this.visibleArea);
108
-
109
- drawText(this._label, context, labelRect, drawableRect, {
110
- fillStyle: COLORS.REACT_RESIZE_BAR_DOT,
111
- textAlign: 'center',
112
- });
113
- } else {
114
- // Otherwise draw horizontally centered resize bar dots
115
- context.beginPath();
116
- context.fillStyle = COLORS.REACT_RESIZE_BAR_DOT;
117
- context.arc(
118
- horizontalCenter,
119
- verticalCenter,
120
- RESIZE_BAR_DOT_RADIUS,
121
- 0,
122
- 2 * Math.PI,
123
- );
124
- context.arc(
125
- horizontalCenter + RESIZE_BAR_DOT_SPACING,
126
- verticalCenter,
127
- RESIZE_BAR_DOT_RADIUS,
128
- 0,
129
- 2 * Math.PI,
130
- );
131
- context.arc(
132
- horizontalCenter - RESIZE_BAR_DOT_SPACING,
133
- verticalCenter,
134
- RESIZE_BAR_DOT_RADIUS,
135
- 0,
136
- 2 * Math.PI,
137
- );
138
- context.fill();
139
- }
140
- }
141
-
142
- _setInteractionState(state: ResizeBarState) {
143
- if (this._interactionState === state) {
144
- return;
145
- }
146
- this._interactionState = state;
147
- this.setNeedsDisplay();
148
- }
149
-
150
- _handleMouseDown(interaction: MouseDownInteraction, viewRefs: ViewRefs) {
151
- const cursorInView = rectContainsPoint(
152
- interaction.payload.location,
153
- this.frame,
154
- );
155
- if (cursorInView) {
156
- this._setInteractionState('dragging');
157
- viewRefs.activeView = this;
158
- }
159
- }
160
-
161
- _handleMouseMove(interaction: MouseMoveInteraction, viewRefs: ViewRefs) {
162
- const cursorInView = rectContainsPoint(
163
- interaction.payload.location,
164
- this.frame,
165
- );
166
-
167
- if (viewRefs.activeView === this) {
168
- // If we're actively dragging this resize bar,
169
- // show the cursor even if the pointer isn't hovering over this view.
170
- this.currentCursor = 'ns-resize';
171
- } else if (cursorInView) {
172
- if (this.showLabel) {
173
- this.currentCursor = 'pointer';
174
- } else {
175
- this.currentCursor = 'ns-resize';
176
- }
177
- }
178
-
179
- if (cursorInView) {
180
- viewRefs.hoveredView = this;
181
- }
182
-
183
- if (this._interactionState === 'dragging') {
184
- return;
185
- }
186
- this._setInteractionState(cursorInView ? 'hovered' : 'normal');
187
- }
188
-
189
- _handleMouseUp(interaction: MouseUpInteraction, viewRefs: ViewRefs) {
190
- const cursorInView = rectContainsPoint(
191
- interaction.payload.location,
192
- this.frame,
193
- );
194
- if (this._interactionState === 'dragging') {
195
- this._setInteractionState(cursorInView ? 'hovered' : 'normal');
196
- }
197
-
198
- if (viewRefs.activeView === this) {
199
- viewRefs.activeView = null;
200
- }
201
- }
202
-
203
- handleInteraction(interaction: Interaction, viewRefs: ViewRefs) {
204
- switch (interaction.type) {
205
- case 'mousedown':
206
- this._handleMouseDown(interaction, viewRefs);
207
- return;
208
- case 'mousemove':
209
- this._handleMouseMove(interaction, viewRefs);
210
- return;
211
- case 'mouseup':
212
- this._handleMouseUp(interaction, viewRefs);
213
- return;
214
- }
215
- }
216
-}
217
-
49
export class ResizableView extends View {
50
_canvasRef: {current: HTMLCanvasElement | null};
51
_layoutState: LayoutState;
52
_mutableViewStateKey: string;
222
- _resizeBar: ResizeBar;
53
+ _resizeBar: ResizeBarView;
54
_resizingState: ResizingState | null = null;
55
_subview: View;
56
_viewState: ViewState;
@@ -238,7 +69,7 @@ export class ResizableView extends View {
69
this._layoutState = {barOffsetY: 0};
70
this._mutableViewStateKey = label + ':ResizableView';
71
this._subview = subview;
241
- this._resizeBar = new ResizeBar(surface, frame, label);
72
+ this._resizeBar = new ResizeBarView(surface, frame, label);
73
this._viewState = viewState;
74
75
this.addSubview(this._subview);
@@ -379,6 +210,8 @@ export class ResizableView extends View {
210
const subviewDesiredSize = this._subview.desiredSize();
211
this._updateLayoutStateAndResizeBar(subviewDesiredSize.height);
212
this.setNeedsDisplay();
213
+
214
+ return true;
215
}
216
}
217
}
@@ -397,6 +230,8 @@ export class ResizableView extends View {
230
// Double clicking on the expanded view should collapse.
231
this._updateLayoutStateAndResizeBar(0);
232
this.setNeedsDisplay();
233
+
234
+ return true;
235
}
236
}
237
}
@@ -410,6 +245,8 @@ export class ResizableView extends View {
245
cursorOffsetInBarFrame: mouseY - resizeBarFrame.origin.y,
246
mouseY,
247
};
248
+
249
+ return true;
250
}
251
}
252
@@ -421,6 +258,8 @@ export class ResizableView extends View {
258
mouseY: interaction.payload.location.y,
259
};
260
this.setNeedsDisplay();
261
+
262
+ return true;
263
}
264
}
265
@@ -443,20 +282,15 @@ export class ResizableView extends View {
282
handleInteraction(interaction: Interaction, viewRefs: ViewRefs) {
283
switch (interaction.type) {
284
case 'click':
446
- this._handleClick(interaction);
447
- return;
285
+ return this._handleClick(interaction);
286
case 'double-click':
449
- this._handleDoubleClick(interaction);
450
- return;
287
+ return this._handleDoubleClick(interaction);
288
case 'mousedown':
452
- this._handleMouseDown(interaction);
453
- return;
289
+ return this._handleMouseDown(interaction);
290
case 'mousemove':
455
- this._handleMouseMove(interaction);
456
- return;
291
+ return this._handleMouseMove(interaction);
292
case 'mouseup':
458
- this._handleMouseUp(interaction);
459
- return;
293
+ return this._handleMouseUp(interaction);
294
}
295
}
296
}
packages/react-devtools-scheduling-profiler/src/view-base/resizable/ResizeBarView.js
new
+193
@@ -0,0 +1,193 @@
1
+/**
2
+ * Copyright (c) Facebook, Inc. and its affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ * @flow
8
+ */
9
+
10
+import type {
11
+ Interaction,
12
+ MouseDownInteraction,
13
+ MouseMoveInteraction,
14
+ MouseUpInteraction,
15
+} from '../useCanvasInteraction';
16
+import type {Rect} from '../geometry';
17
+import type {ViewRefs} from '../Surface';
18
+
19
+import {BORDER_SIZE, COLORS} from '../../content-views/constants';
20
+import {drawText} from '../../content-views/utils/text';
21
+import {Surface} from '../Surface';
22
+import {View} from '../View';
23
+import {rectContainsPoint} from '../geometry';
24
+import {noopLayout} from '../layouter';
25
+
26
+type ResizeBarState = 'normal' | 'hovered' | 'dragging';
27
+
28
+const RESIZE_BAR_DOT_RADIUS = 1;
29
+const RESIZE_BAR_DOT_SPACING = 4;
30
+const RESIZE_BAR_HEIGHT = 8;
31
+const RESIZE_BAR_WITH_LABEL_HEIGHT = 16;
32
+
33
+export class ResizeBarView extends View {
34
+ _interactionState: ResizeBarState = 'normal';
35
+ _label: string;
36
+
37
+ showLabel: boolean = false;
38
+
39
+ constructor(surface: Surface, frame: Rect, label: string) {
40
+ super(surface, frame, noopLayout);
41
+
42
+ this._label = label;
43
+ }
44
+
45
+ desiredSize() {
46
+ return this.showLabel
47
+ ? {height: RESIZE_BAR_WITH_LABEL_HEIGHT, width: 0}
48
+ : {height: RESIZE_BAR_HEIGHT, width: 0};
49
+ }
50
+
51
+ draw(context: CanvasRenderingContext2D, viewRefs: ViewRefs) {
52
+ const {frame} = this;
53
+ const {x, y} = frame.origin;
54
+ const {width, height} = frame.size;
55
+
56
+ const isActive =
57
+ this._interactionState === 'dragging' ||
58
+ (this._interactionState === 'hovered' && viewRefs.activeView === null);
59
+
60
+ context.fillStyle = isActive
61
+ ? COLORS.REACT_RESIZE_BAR_ACTIVE
62
+ : COLORS.REACT_RESIZE_BAR;
63
+ context.fillRect(x, y, width, height);
64
+
65
+ context.fillStyle = COLORS.REACT_RESIZE_BAR_BORDER;
66
+ context.fillRect(x, y, width, BORDER_SIZE);
67
+ context.fillRect(x, y + height - BORDER_SIZE, width, BORDER_SIZE);
68
+
69
+ const horizontalCenter = x + width / 2;
70
+ const verticalCenter = y + height / 2;
71
+
72
+ if (this.showLabel) {
73
+ // When the resize view is collapsed entirely,
74
+ // rather than showing a resize bar– this view displays a label.
75
+ const labelRect: Rect = {
76
+ origin: {
77
+ x: 0,
78
+ y: y + height - RESIZE_BAR_WITH_LABEL_HEIGHT,
79
+ },
80
+ size: {
81
+ width: frame.size.width,
82
+ height: RESIZE_BAR_WITH_LABEL_HEIGHT,
83
+ },
84
+ };
85
+
86
+ drawText(this._label, context, labelRect, frame, {
87
+ fillStyle: COLORS.REACT_RESIZE_BAR_DOT,
88
+ textAlign: 'center',
89
+ });
90
+ } else {
91
+ // Otherwise draw horizontally centered resize bar dots
92
+ context.beginPath();
93
+ context.fillStyle = COLORS.REACT_RESIZE_BAR_DOT;
94
+ context.arc(
95
+ horizontalCenter,
96
+ verticalCenter,
97
+ RESIZE_BAR_DOT_RADIUS,
98
+ 0,
99
+ 2 * Math.PI,
100
+ );
101
+ context.arc(
102
+ horizontalCenter + RESIZE_BAR_DOT_SPACING,
103
+ verticalCenter,
104
+ RESIZE_BAR_DOT_RADIUS,
105
+ 0,
106
+ 2 * Math.PI,
107
+ );
108
+ context.arc(
109
+ horizontalCenter - RESIZE_BAR_DOT_SPACING,
110
+ verticalCenter,
111
+ RESIZE_BAR_DOT_RADIUS,
112
+ 0,
113
+ 2 * Math.PI,
114
+ );
115
+ context.fill();
116
+ }
117
+ }
118
+
119
+ _setInteractionState(state: ResizeBarState) {
120
+ if (this._interactionState === state) {
121
+ return;
122
+ }
123
+ this._interactionState = state;
124
+ this.setNeedsDisplay();
125
+ }
126
+
127
+ _handleMouseDown(interaction: MouseDownInteraction, viewRefs: ViewRefs) {
128
+ const cursorInView = rectContainsPoint(
129
+ interaction.payload.location,
130
+ this.frame,
131
+ );
132
+ if (cursorInView) {
133
+ this._setInteractionState('dragging');
134
+ viewRefs.activeView = this;
135
+ }
136
+ }
137
+
138
+ _handleMouseMove(interaction: MouseMoveInteraction, viewRefs: ViewRefs) {
139
+ const cursorInView = rectContainsPoint(
140
+ interaction.payload.location,
141
+ this.frame,
142
+ );
143
+
144
+ if (viewRefs.activeView === this) {
145
+ // If we're actively dragging this resize bar,
146
+ // show the cursor even if the pointer isn't hovering over this view.
147
+ this.currentCursor = 'ns-resize';
148
+ } else if (cursorInView) {
149
+ if (this.showLabel) {
150
+ this.currentCursor = 'pointer';
151
+ } else {
152
+ this.currentCursor = 'ns-resize';
153
+ }
154
+ }
155
+
156
+ if (cursorInView) {
157
+ viewRefs.hoveredView = this;
158
+ }
159
+
160
+ if (this._interactionState === 'dragging') {
161
+ return;
162
+ }
163
+ this._setInteractionState(cursorInView ? 'hovered' : 'normal');
164
+ }
165
+
166
+ _handleMouseUp(interaction: MouseUpInteraction, viewRefs: ViewRefs) {
167
+ const cursorInView = rectContainsPoint(
168
+ interaction.payload.location,
169
+ this.frame,
170
+ );
171
+ if (this._interactionState === 'dragging') {
172
+ this._setInteractionState(cursorInView ? 'hovered' : 'normal');
173
+ }
174
+
175
+ if (viewRefs.activeView === this) {
176
+ viewRefs.activeView = null;
177
+ }
178
+ }
179
+
180
+ handleInteraction(interaction: Interaction, viewRefs: ViewRefs) {
181
+ switch (interaction.type) {
182
+ case 'mousedown':
183
+ this._handleMouseDown(interaction, viewRefs);
184
+ break;
185
+ case 'mousemove':
186
+ this._handleMouseMove(interaction, viewRefs);
187
+ break;
188
+ case 'mouseup':
189
+ this._handleMouseUp(interaction, viewRefs);
190
+ break;
191
+ }
192
+ }
193
+}
packages/react-devtools-scheduling-profiler/src/view-base/resizable/index.js
new
+11
@@ -0,0 +1,11 @@
1
+/**
2
+ * Copyright (c) Facebook, Inc. and its affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ * @flow
8
+ */
9
+
10
+export * from './ResizableView';
11
+export * from './ResizeBarView';
packages/react-devtools-scheduling-profiler/src/view-base/vertical-scroll-overflow/VerticalScrollBarView.js
new
+219
@@ -0,0 +1,219 @@
1
+/**
2
+ * Copyright (c) Facebook, Inc. and its affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ * @flow
8
+ */
9
+
10
+import type {Interaction} from '../useCanvasInteraction';
11
+import type {Rect} from '../geometry';
12
+import type {Surface, ViewRefs} from '../Surface';
13
+import type {
14
+ ClickInteraction,
15
+ MouseDownInteraction,
16
+ MouseMoveInteraction,
17
+ MouseUpInteraction,
18
+} from '../useCanvasInteraction';
19
+
20
+import {VerticalScrollOverflowView} from './VerticalScrollOverflowView';
21
+import {rectContainsPoint, rectEqualToRect} from '../geometry';
22
+import {View} from '../View';
23
+import {BORDER_SIZE, COLORS} from '../../content-views/constants';
24
+
25
+const SCROLL_BAR_SIZE = 14;
26
+
27
+const HIDDEN_RECT = {
28
+ origin: {
29
+ x: 0,
30
+ y: 0,
31
+ },
32
+ size: {
33
+ width: 0,
34
+ height: 0,
35
+ },
36
+};
37
+
38
+export class VerticalScrollBarView extends View {
39
+ _contentHeight: number = 0;
40
+ _isScrolling: boolean = false;
41
+ _scrollBarRect: Rect = HIDDEN_RECT;
42
+ _scrollThumbRect: Rect = HIDDEN_RECT;
43
+ _verticalScrollOverflowView: VerticalScrollOverflowView;
44
+
45
+ constructor(
46
+ surface: Surface,
47
+ frame: Rect,
48
+ verticalScrollOverflowView: VerticalScrollOverflowView,
49
+ ) {
50
+ super(surface, frame);
51
+
52
+ this._verticalScrollOverflowView = verticalScrollOverflowView;
53
+ }
54
+
55
+ desiredSize() {
56
+ return {
57
+ width: SCROLL_BAR_SIZE,
58
+ height: 0, // No desired height
59
+ };
60
+ }
61
+
62
+ getMaxScrollThumbY(): number {
63
+ const {height} = this.frame.size;
64
+
65
+ const maxScrollThumbY = height - this._scrollThumbRect.size.height;
66
+
67
+ return maxScrollThumbY;
68
+ }
69
+
70
+ setContentHeight(contentHeight: number) {
71
+ this._contentHeight = contentHeight;
72
+
73
+ const {height, width} = this.frame.size;
74
+
75
+ const proposedScrollThumbRect = {
76
+ origin: {
77
+ x: this.frame.origin.x,
78
+ y: this._scrollThumbRect.origin.y,
79
+ },
80
+ size: {
81
+ width,
82
+ height: height * (height / contentHeight),
83
+ },
84
+ };
85
+
86
+ if (!rectEqualToRect(this._scrollThumbRect, proposedScrollThumbRect)) {
87
+ this._scrollThumbRect = proposedScrollThumbRect;
88
+ this.setNeedsDisplay();
89
+ }
90
+ }
91
+
92
+ setScrollThumbY(value: number) {
93
+ const {height} = this.frame.size;
94
+
95
+ const maxScrollThumbY = this.getMaxScrollThumbY();
96
+ const newScrollThumbY = Math.max(0, Math.min(maxScrollThumbY, value));
97
+
98
+ this._scrollThumbRect = {
99
+ ...this._scrollThumbRect,
100
+ origin: {
101
+ x: this.frame.origin.x,
102
+ y: newScrollThumbY,
103
+ },
104
+ };
105
+
106
+ const maxContentOffset = this._contentHeight - height;
107
+ const contentScrollOffset =
108
+ (newScrollThumbY / maxScrollThumbY) * maxContentOffset * -1;
109
+
110
+ this._verticalScrollOverflowView.setScrollOffset(
111
+ contentScrollOffset,
112
+ maxScrollThumbY,
113
+ );
114
+ }
115
+
116
+ draw(context: CanvasRenderingContext2D, viewRefs: ViewRefs) {
117
+ const {x, y} = this.frame.origin;
118
+ const {width, height} = this.frame.size;
119
+
120
+ // TODO Use real color
121
+ context.fillStyle = COLORS.REACT_RESIZE_BAR;
122
+ context.fillRect(x, y, width, height);
123
+
124
+ // TODO Use real color
125
+ context.fillStyle = COLORS.SCROLL_CARET;
126
+ context.fillRect(
127
+ this._scrollThumbRect.origin.x,
128
+ this._scrollThumbRect.origin.y,
129
+ this._scrollThumbRect.size.width,
130
+ this._scrollThumbRect.size.height,
131
+ );
132
+
133
+ // TODO Use real color
134
+ context.fillStyle = COLORS.REACT_RESIZE_BAR_BORDER;
135
+ context.fillRect(x, y, BORDER_SIZE, height);
136
+ }
137
+
138
+ handleInteraction(interaction: Interaction, viewRefs: ViewRefs) {
139
+ switch (interaction.type) {
140
+ case 'click':
141
+ this._handleClick(interaction, viewRefs);
142
+ break;
143
+ case 'mousedown':
144
+ this._handleMouseDown(interaction, viewRefs);
145
+ break;
146
+ case 'mousemove':
147
+ this._handleMouseMove(interaction, viewRefs);
148
+ break;
149
+ case 'mouseup':
150
+ this._handleMouseUp(interaction, viewRefs);
151
+ break;
152
+ }
153
+ }
154
+
155
+ _handleClick(interaction: ClickInteraction, viewRefs: ViewRefs) {
156
+ const {location} = interaction.payload;
157
+ if (rectContainsPoint(location, this.frame)) {
158
+ if (rectContainsPoint(location, this._scrollThumbRect)) {
159
+ // Ignore clicks on the track thumb directly.
160
+ return;
161
+ }
162
+
163
+ const currentScrollThumbY = this._scrollThumbRect.origin.y;
164
+ const y = location.y;
165
+
166
+ const {height} = this.frame.size;
167
+
168
+ // Scroll up or down about one viewport worth of content:
169
+ const deltaY = (height / this._contentHeight) * height * 0.8;
170
+
171
+ this.setScrollThumbY(
172
+ y > currentScrollThumbY
173
+ ? this._scrollThumbRect.origin.y + deltaY
174
+ : this._scrollThumbRect.origin.y - deltaY,
175
+ );
176
+ }
177
+ }
178
+
179
+ _handleMouseDown(interaction: MouseDownInteraction, viewRefs: ViewRefs) {
180
+ const {location} = interaction.payload;
181
+ if (!rectContainsPoint(location, this._scrollThumbRect)) {
182
+ return;
183
+ }
184
+ viewRefs.activeView = this;
185
+
186
+ this.currentCursor = 'default';
187
+
188
+ this._isScrolling = true;
189
+ this.setNeedsDisplay();
190
+ }
191
+
192
+ _handleMouseMove(interaction: MouseMoveInteraction, viewRefs: ViewRefs) {
193
+ const {event, location} = interaction.payload;
194
+ if (rectContainsPoint(location, this.frame)) {
195
+ if (viewRefs.hoveredView !== this) {
196
+ viewRefs.hoveredView = this;
197
+ }
198
+
199
+ this.currentCursor = 'default';
200
+ }
201
+
202
+ if (viewRefs.activeView === this) {
203
+ this.currentCursor = 'default';
204
+
205
+ this.setScrollThumbY(this._scrollThumbRect.origin.y + event.movementY);
206
+ }
207
+ }
208
+
209
+ _handleMouseUp(interaction: MouseUpInteraction, viewRefs: ViewRefs) {
210
+ if (viewRefs.activeView === this) {
211
+ viewRefs.activeView = null;
212
+ }
213
+
214
+ if (this._isScrolling) {
215
+ this._isScrolling = false;
216
+ this.setNeedsDisplay();
217
+ }
218
+ }
219
+}
packages/react-devtools-scheduling-profiler/src/view-base/vertical-scroll-overflow/VerticalScrollOverflowView.js
new
+91
@@ -0,0 +1,91 @@
1
+/**
2
+ * Copyright (c) Facebook, Inc. and its affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ * @flow
8
+ */
9
+
10
+import type {Rect} from '../geometry';
11
+import type {ScrollState} from '../utils/scrollState';
12
+import type {Surface} from '../Surface';
13
+import type {ViewState} from '../../types';
14
+
15
+import {VerticalScrollBarView} from './VerticalScrollBarView';
16
+import {withVerticalScrollbarLayout} from './withVerticalScrollbarLayout';
17
+import {View} from '../View';
18
+import {VerticalScrollView} from '../VerticalScrollView';
19
+
20
+export class VerticalScrollOverflowView extends View {
21
+ _contentView: View;
22
+ _isProcessingOnChange: boolean = false;
23
+ _isScrolling: boolean = false;
24
+ _scrollOffset: number = 0;
25
+ _scrollBarView: VerticalScrollBarView;
26
+ _verticalScrollView: VerticalScrollView;
27
+
28
+ constructor(
29
+ surface: Surface,
30
+ frame: Rect,
31
+ contentView: View,
32
+ viewState: ViewState,
33
+ ) {
34
+ super(surface, frame, withVerticalScrollbarLayout);
35
+
36
+ this._contentView = contentView;
37
+ this._verticalScrollView = new VerticalScrollView(
38
+ surface,
39
+ frame,
40
+ contentView,
41
+ viewState,
42
+ 'VerticalScrollOverflowView',
43
+ );
44
+ this._verticalScrollView.onChange(this._onVerticalScrollViewChange);
45
+
46
+ this._scrollBarView = new VerticalScrollBarView(surface, frame, this);
47
+
48
+ this.addSubview(this._verticalScrollView);
49
+ this.addSubview(this._scrollBarView);
50
+ }
51
+
52
+ layoutSubviews() {
53
+ super.layoutSubviews();
54
+
55
+ const contentSize = this._contentView.desiredSize();
56
+
57
+ // This should be done after calling super.layoutSubviews() – calling it
58
+ // before somehow causes _contentView to need display on every mousemove
59
+ // event when the scroll bar is shown.
60
+ this._scrollBarView.setContentHeight(contentSize.height);
61
+ }
62
+
63
+ setScrollOffset(newScrollOffset: number, maxScrollOffset: number) {
64
+ const deltaY = newScrollOffset - this._scrollOffset;
65
+
66
+ if (!this._isProcessingOnChange) {
67
+ this._verticalScrollView.scrollBy(-deltaY);
68
+ }
69
+
70
+ this._scrollOffset = newScrollOffset;
71
+
72
+ this.setNeedsDisplay();
73
+ }
74
+
75
+ _onVerticalScrollViewChange = (
76
+ scrollState: ScrollState,
77
+ containerLength: number,
78
+ ) => {
79
+ const maxOffset = scrollState.length - containerLength;
80
+ if (maxOffset === 0) {
81
+ return;
82
+ }
83
+
84
+ const percentage = Math.abs(scrollState.offset) / maxOffset;
85
+ const maxScrollThumbY = this._scrollBarView.getMaxScrollThumbY();
86
+
87
+ this._isProcessingOnChange = true;
88
+ this._scrollBarView.setScrollThumbY(percentage * maxScrollThumbY);
89
+ this._isProcessingOnChange = false;
90
+ };
91
+}
packages/react-devtools-scheduling-profiler/src/view-base/vertical-scroll-overflow/index.js
new
+11
@@ -0,0 +1,11 @@
1
+/**
2
+ * Copyright (c) Facebook, Inc. and its affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ * @flow
8
+ */
9
+
10
+export * from './VerticalScrollBarView';
11
+export * from './VerticalScrollOverflowView';
packages/react-devtools-scheduling-profiler/src/view-base/vertical-scroll-overflow/withVerticalScrollbarLayout.js
new
+55
@@ -0,0 +1,55 @@
1
+/**
2
+ * Copyright (c) Facebook, Inc. and its affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ * @flow
8
+ */
9
+
10
+import type {Layouter} from '../layouter';
11
+
12
+/**
13
+ * Assumes {@param layout} will only contain 2 views.
14
+ */
15
+export const withVerticalScrollbarLayout: Layouter = (
16
+ layout,
17
+ containerFrame,
18
+) => {
19
+ const [contentLayoutInfo, scrollbarLayoutInfo] = layout;
20
+
21
+ const desiredContentSize = contentLayoutInfo.view.desiredSize();
22
+ const shouldShowScrollbar =
23
+ desiredContentSize.height > containerFrame.size.height;
24
+ const scrollbarWidth = shouldShowScrollbar
25
+ ? scrollbarLayoutInfo.view.desiredSize().width
26
+ : 0;
27
+
28
+ const laidOutContentLayoutInfo = {
29
+ ...contentLayoutInfo,
30
+ frame: {
31
+ origin: contentLayoutInfo.view.frame.origin,
32
+ size: {
33
+ width: containerFrame.size.width - scrollbarWidth,
34
+ height: containerFrame.size.height,
35
+ },
36
+ },
37
+ };
38
+ const laidOutScrollbarLayoutInfo = {
39
+ ...scrollbarLayoutInfo,
40
+ frame: {
41
+ origin: {
42
+ x:
43
+ laidOutContentLayoutInfo.frame.origin.x +
44
+ laidOutContentLayoutInfo.frame.size.width,
45
+ y: containerFrame.origin.y,
46
+ },
47
+ size: {
48
+ width: scrollbarWidth,
49
+ height: containerFrame.size.height,
50
+ },
51
+ },
52
+ };
53
+
54
+ return [laidOutContentLayoutInfo, laidOutScrollbarLayoutInfo];
55
+};