Skip to content

Commit 280e3f9

Browse files
Brian Vaughntaneliang
Brian Vaughn
andauthored
DevTools: Scheduling profiler: Add vertical scroll bar (#22005)
Co-authored-by: E-Liang Tan <[email protected]>
1 parent 88d1218 commit 280e3f9

File tree

14 files changed

+731
-284
lines changed

14 files changed

+731
-284
lines changed

packages/react-devtools-scheduling-profiler/src/CanvasPage.js

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -30,14 +30,12 @@ import {copy} from 'clipboard-js';
3030
import prettyMilliseconds from 'pretty-ms';
3131

3232
import {
33-
BackgroundColorView,
3433
HorizontalPanAndZoomView,
3534
ResizableView,
35+
VerticalScrollOverflowView,
3636
Surface,
3737
VerticalScrollView,
3838
View,
39-
createComposedLayout,
40-
lastViewTakesUpRemainingSpaceLayout,
4139
useCanvasInteraction,
4240
verticallyStackedLayout,
4341
zeroPoint,
@@ -325,10 +323,9 @@ function AutoSizedCanvas({
325323
const rootView = new View(
326324
surface,
327325
defaultFrame,
328-
createComposedLayout(
329-
verticallyStackedLayout,
330-
lastViewTakesUpRemainingSpaceLayout,
331-
),
326+
verticallyStackedLayout,
327+
defaultFrame,
328+
COLORS.BACKGROUND,
332329
);
333330
rootView.addSubview(axisMarkersViewWrapper);
334331
if (userTimingMarksViewWrapper !== null) {
@@ -345,10 +342,14 @@ function AutoSizedCanvas({
345342
}
346343
rootView.addSubview(flamechartViewWrapper);
347344

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+
);
350351

351-
surfaceRef.current.rootView = rootView;
352+
surfaceRef.current.rootView = verticalScrollOverflowView;
352353
}, [data]);
353354

354355
useLayoutEffect(() => {
@@ -401,6 +402,16 @@ function AutoSizedCanvas({
401402
const surface = surfaceRef.current;
402403
surface.handleInteraction(interaction);
403404

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+
404415
canvas.style.cursor = surface.getCurrentCursor() || 'default';
405416

406417
// Defer drawing to canvas until React's commit phase, to avoid drawing

packages/react-devtools-scheduling-profiler/src/import-worker/preprocessData.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -241,9 +241,11 @@ function processTimelineEvent(
241241

242242
// Reduce noise from events like DOMActivate, load/unload, etc. which are usually not relevant
243243
if (
244-
type.startsWith('blur') ||
245-
type.startsWith('click') ||
244+
type === 'blur' ||
245+
type === 'click' ||
246+
type === 'input' ||
246247
type.startsWith('focus') ||
248+
type.startsWith('key') ||
247249
type.startsWith('mouse') ||
248250
type.startsWith('pointer')
249251
) {

packages/react-devtools-scheduling-profiler/src/view-base/VerticalScrollOverflowView.js

Lines changed: 0 additions & 3 deletions
This file was deleted.

packages/react-devtools-scheduling-profiler/src/view-base/VerticalScrollView.js

Lines changed: 52 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -34,10 +34,16 @@ const CARET_MARGIN = 3;
3434
const CARET_WIDTH = 5;
3535
const CARET_HEIGHT = 3;
3636

37+
type OnChangeCallback = (
38+
scrollState: ScrollState,
39+
containerLength: number,
40+
) => void;
41+
3742
export class VerticalScrollView extends View {
3843
_contentView: View;
3944
_isPanning: boolean;
4045
_mutableViewStateKey: string;
46+
_onChangeCallback: OnChangeCallback | null;
4147
_scrollState: ScrollState;
4248
_viewState: ViewState;
4349

@@ -53,6 +59,7 @@ export class VerticalScrollView extends View {
5359
this._contentView = contentView;
5460
this._isPanning = false;
5561
this._mutableViewStateKey = label + ':VerticalScrollView';
62+
this._onChangeCallback = null;
5663
this._scrollState = {
5764
offset: 0,
5865
length: 0,
@@ -146,26 +153,45 @@ export class VerticalScrollView extends View {
146153
super.layoutSubviews();
147154
}
148155

149-
handleInteraction(interaction: Interaction) {
156+
handleInteraction(interaction: Interaction): ?boolean {
150157
switch (interaction.type) {
151158
case 'mousedown':
152-
this._handleMouseDown(interaction);
153-
break;
159+
return this._handleMouseDown(interaction);
154160
case 'mousemove':
155-
this._handleMouseMove(interaction);
156-
break;
161+
return this._handleMouseMove(interaction);
157162
case 'mouseup':
158-
this._handleMouseUp(interaction);
159-
break;
163+
return this._handleMouseUp(interaction);
160164
case 'wheel-shift':
161-
this._handleWheelShift(interaction);
162-
break;
165+
return this._handleWheelShift(interaction);
163166
}
164167
}
165168

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+
166186
_handleMouseDown(interaction: MouseDownInteraction) {
167187
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+
}
169195
}
170196
}
171197

@@ -179,6 +205,7 @@ export class VerticalScrollView extends View {
179205
containerLength: this.frame.size.height,
180206
});
181207
this._setScrollState(newState);
208+
return true;
182209
}
183210

184211
_handleMouseUp(interaction: MouseUpInteraction) {
@@ -187,31 +214,27 @@ export class VerticalScrollView extends View {
187214
}
188215
}
189216

190-
_handleWheelShift(interaction: WheelWithShiftInteraction) {
217+
_handleWheelShift(interaction: WheelWithShiftInteraction): boolean {
191218
const {
192219
location,
193220
delta: {deltaX, deltaY},
194221
} = interaction.payload;
222+
195223
if (!rectContainsPoint(location, this.frame)) {
196-
return; // Not scrolling on view
224+
return false; // Not scrolling on view
197225
}
198226

199227
const absDeltaX = Math.abs(deltaX);
200228
const absDeltaY = Math.abs(deltaY);
201229
if (absDeltaX > absDeltaY) {
202-
return; // Scrolling horizontally
230+
return false; // Scrolling horizontally
203231
}
204232

205233
if (absDeltaY < MOVE_WHEEL_DELTA_THRESHOLD) {
206-
return;
234+
return false; // Movement was too small and should be ignored.
207235
}
208236

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);
215238
}
216239

217240
_restoreMutableViewState() {
@@ -231,10 +254,7 @@ export class VerticalScrollView extends View {
231254
this.setNeedsDisplay();
232255
}
233256

234-
/**
235-
* @private
236-
*/
237-
_setScrollState(proposedState: ScrollState) {
257+
_setScrollState(proposedState: ScrollState): boolean {
238258
const height = this._contentView.frame.size.height;
239259
const clampedState = clampState({
240260
state: proposedState,
@@ -247,6 +267,14 @@ export class VerticalScrollView extends View {
247267
this._scrollState.length = clampedState.length;
248268

249269
this.setNeedsDisplay();
270+
271+
if (this._onChangeCallback !== null) {
272+
this._onChangeCallback(clampedState, this.frame.size.height);
273+
}
274+
275+
return true;
250276
}
277+
278+
return false;
251279
}
252280
}

packages/react-devtools-scheduling-profiler/src/view-base/View.js

Lines changed: 46 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ import {noopLayout, viewsToLayout, collapseLayoutIntoViews} from './layouter';
2929
* subclasses.
3030
*/
3131
export class View {
32+
_backgroundColor: string | null;
33+
3234
currentCursor: string | null = null;
3335

3436
surface: Surface;
@@ -70,7 +72,9 @@ export class View {
7072
frame: Rect,
7173
layouter: Layouter = noopLayout,
7274
visibleArea: Rect = frame,
75+
backgroundColor?: string | null = null,
7376
) {
77+
this._backgroundColor = backgroundColor || null;
7478
this.surface = surface;
7579
this.frame = frame;
7680
this._layouter = layouter;
@@ -246,6 +250,20 @@ export class View {
246250
subview.displayIfNeeded(context, viewRefs);
247251
}
248252
});
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+
}
249267
}
250268

251269
/**
@@ -255,7 +273,7 @@ export class View {
255273
*
256274
* NOTE: Do not call directly! Use `handleInteractionAndPropagateToSubviews`
257275
*/
258-
handleInteraction(interaction: Interaction, viewRefs: ViewRefs) {}
276+
handleInteraction(interaction: Interaction, viewRefs: ViewRefs): ?boolean {}
259277

260278
/**
261279
* Handle an `interaction` and propagates it to all of this view's
@@ -270,19 +288,39 @@ export class View {
270288
handleInteractionAndPropagateToSubviews(
271289
interaction: Interaction,
272290
viewRefs: ViewRefs,
273-
) {
291+
): boolean {
274292
const {subviews, visibleArea} = this;
275293

276294
if (visibleArea.size.height === 0) {
277-
return;
295+
return false;
278296
}
279297

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];
283306
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+
}
285315
}
286-
});
316+
}
317+
318+
const didSelfHandle =
319+
this.handleInteraction(interaction, viewRefs) === true;
320+
if (didSelfHandle) {
321+
return true;
322+
}
323+
324+
return false;
287325
}
288326
}

packages/react-devtools-scheduling-profiler/src/view-base/index.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,11 @@
99

1010
export * from './BackgroundColorView';
1111
export * from './HorizontalPanAndZoomView';
12-
export * from './ResizableView';
1312
export * from './Surface';
1413
export * from './VerticalScrollView';
1514
export * from './View';
1615
export * from './geometry';
1716
export * from './layouter';
17+
export * from './resizable';
1818
export * from './useCanvasInteraction';
19+
export * from './vertical-scroll-overflow';

packages/react-devtools-scheduling-profiler/src/view-base/layouter.js

Lines changed: 0 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -206,50 +206,6 @@ export const atLeastContainerHeightLayout: Layouter = (
206206
}));
207207
};
208208

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-
253209
/**
254210
* Create a layouter that applies each layouter in `layouters` in sequence.
255211
*/

0 commit comments

Comments
 (0)