Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

DevTools: Scheduling profiler: Add vertical scroll bar #22005

Merged
merged 15 commits into from
Aug 11, 2021
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 17 additions & 10 deletions packages/react-devtools-scheduling-profiler/src/CanvasPage.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,14 +30,12 @@ import {copy} from 'clipboard-js';
import prettyMilliseconds from 'pretty-ms';

import {
BackgroundColorView,
HorizontalPanAndZoomView,
ResizableView,
VerticalScrollOverflowView,
Surface,
VerticalScrollView,
View,
createComposedLayout,
lastViewTakesUpRemainingSpaceLayout,
useCanvasInteraction,
verticallyStackedLayout,
zeroPoint,
Expand Down Expand Up @@ -325,10 +323,9 @@ function AutoSizedCanvas({
const rootView = new View(
surface,
defaultFrame,
createComposedLayout(
verticallyStackedLayout,
lastViewTakesUpRemainingSpaceLayout,
),
verticallyStackedLayout,
defaultFrame,
COLORS.BACKGROUND,
);
rootView.addSubview(axisMarkersViewWrapper);
if (userTimingMarksViewWrapper !== null) {
Expand All @@ -345,10 +342,14 @@ function AutoSizedCanvas({
}
rootView.addSubview(flamechartViewWrapper);

// If subviews are less than the available height, fill remaining height with a solid color.
rootView.addSubview(new BackgroundColorView(surface, defaultFrame));
const verticalScrollOverflowView = new VerticalScrollOverflowView(
surface,
defaultFrame,
rootView,
viewState,
);

surfaceRef.current.rootView = rootView;
surfaceRef.current.rootView = verticalScrollOverflowView;
}, [data]);

useLayoutEffect(() => {
Expand Down Expand Up @@ -401,6 +402,12 @@ function AutoSizedCanvas({
const surface = surfaceRef.current;
surface.handleInteraction(interaction);

// Flush any display work that got queued up as part of the previous interaction.
// Typically there should be no work, but certain interactions may need a second pass.
// For example, the ResizableView may collapse/expand its contents,
// which requires a second layout pass for an ancestor VerticalScrollOverflowView.
surface.displayIfNeeded();

canvas.style.cursor = surface.getCurrentCursor() || 'default';

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

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,16 @@ const CARET_MARGIN = 3;
const CARET_WIDTH = 5;
const CARET_HEIGHT = 3;

type OnChangeCallback = (
scrollState: ScrollState,
containerLength: number,
) => void;

export class VerticalScrollView extends View {
_contentView: View;
_isPanning: boolean;
_mutableViewStateKey: string;
_onChangeCallback: OnChangeCallback | null;
_scrollState: ScrollState;
_viewState: ViewState;

Expand All @@ -53,6 +59,7 @@ export class VerticalScrollView extends View {
this._contentView = contentView;
this._isPanning = false;
this._mutableViewStateKey = label + ':VerticalScrollView';
this._onChangeCallback = null;
this._scrollState = {
offset: 0,
length: 0,
Expand Down Expand Up @@ -146,26 +153,45 @@ export class VerticalScrollView extends View {
super.layoutSubviews();
}

handleInteraction(interaction: Interaction) {
handleInteraction(interaction: Interaction): ?boolean {
switch (interaction.type) {
case 'mousedown':
this._handleMouseDown(interaction);
break;
return this._handleMouseDown(interaction);
case 'mousemove':
this._handleMouseMove(interaction);
break;
return this._handleMouseMove(interaction);
case 'mouseup':
this._handleMouseUp(interaction);
break;
return this._handleMouseUp(interaction);
case 'wheel-shift':
this._handleWheelShift(interaction);
break;
return this._handleWheelShift(interaction);
}
}

onChange(callback: OnChangeCallback) {
this._onChangeCallback = callback;
}

scrollBy(deltaY: number): boolean {
const newState = translateState({
state: this._scrollState,
delta: -deltaY,
containerLength: this.frame.size.height,
});

// If the state is updated by this wheel scroll,
// return true to prevent the interaction from bubbling.
// For instance, this prevents the outermost container from also scrolling.
return this._setScrollState(newState);
}

_handleMouseDown(interaction: MouseDownInteraction) {
if (rectContainsPoint(interaction.payload.location, this.frame)) {
this._isPanning = true;
const frameHeight = this.frame.size.height;
const contentHeight = this._contentView.desiredSize().height;
// Don't claim drag operations if the content is not tall enough to be scrollable.
// This would block any outer scroll views from working.
if (frameHeight < contentHeight) {
this._isPanning = true;
}
}
}

Expand All @@ -179,6 +205,7 @@ export class VerticalScrollView extends View {
containerLength: this.frame.size.height,
});
this._setScrollState(newState);
return true;
}

_handleMouseUp(interaction: MouseUpInteraction) {
Expand All @@ -187,31 +214,27 @@ export class VerticalScrollView extends View {
}
}

_handleWheelShift(interaction: WheelWithShiftInteraction) {
_handleWheelShift(interaction: WheelWithShiftInteraction): boolean {
const {
location,
delta: {deltaX, deltaY},
} = interaction.payload;

if (!rectContainsPoint(location, this.frame)) {
return; // Not scrolling on view
return false; // Not scrolling on view
}

const absDeltaX = Math.abs(deltaX);
const absDeltaY = Math.abs(deltaY);
if (absDeltaX > absDeltaY) {
return; // Scrolling horizontally
return false; // Scrolling horizontally
}

if (absDeltaY < MOVE_WHEEL_DELTA_THRESHOLD) {
return;
return false; // Movement was too small and should be ignored.
}

const newState = translateState({
state: this._scrollState,
delta: -deltaY,
containerLength: this.frame.size.height,
});
this._setScrollState(newState);
return this.scrollBy(deltaY);
}

_restoreMutableViewState() {
Expand All @@ -231,10 +254,7 @@ export class VerticalScrollView extends View {
this.setNeedsDisplay();
}

/**
* @private
*/
_setScrollState(proposedState: ScrollState) {
_setScrollState(proposedState: ScrollState): boolean {
const height = this._contentView.frame.size.height;
const clampedState = clampState({
state: proposedState,
Expand All @@ -247,6 +267,14 @@ export class VerticalScrollView extends View {
this._scrollState.length = clampedState.length;

this.setNeedsDisplay();

if (this._onChangeCallback !== null) {
this._onChangeCallback(clampedState, this.frame.size.height);
}

return true;
}

return false;
}
}
54 changes: 46 additions & 8 deletions packages/react-devtools-scheduling-profiler/src/view-base/View.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ import {noopLayout, viewsToLayout, collapseLayoutIntoViews} from './layouter';
* subclasses.
*/
export class View {
_backgroundColor: string | null;

currentCursor: string | null = null;

surface: Surface;
Expand Down Expand Up @@ -70,7 +72,9 @@ export class View {
frame: Rect,
layouter: Layouter = noopLayout,
visibleArea: Rect = frame,
backgroundColor?: string | null = null,
) {
this._backgroundColor = backgroundColor || null;
this.surface = surface;
this.frame = frame;
this._layouter = layouter;
Expand Down Expand Up @@ -246,6 +250,20 @@ export class View {
subview.displayIfNeeded(context, viewRefs);
}
});

const backgroundColor = this._backgroundColor;
if (backgroundColor !== null) {
const desiredSize = this.desiredSize();
if (visibleArea.size.height > desiredSize.height) {
context.fillStyle = backgroundColor;
context.fillRect(
visibleArea.origin.x,
visibleArea.origin.y + desiredSize.height,
visibleArea.size.width,
visibleArea.size.height - desiredSize.height,
);
}
}
}

/**
Expand All @@ -255,7 +273,7 @@ export class View {
*
* NOTE: Do not call directly! Use `handleInteractionAndPropagateToSubviews`
*/
handleInteraction(interaction: Interaction, viewRefs: ViewRefs) {}
handleInteraction(interaction: Interaction, viewRefs: ViewRefs): ?boolean {}

/**
* Handle an `interaction` and propagates it to all of this view's
Expand All @@ -270,19 +288,39 @@ export class View {
handleInteractionAndPropagateToSubviews(
interaction: Interaction,
viewRefs: ViewRefs,
) {
): boolean {
const {subviews, visibleArea} = this;

if (visibleArea.size.height === 0) {
return;
return false;
}

this.handleInteraction(interaction, viewRefs);

subviews.forEach(subview => {
// Pass the interaction to subviews first,
// so they have the opportunity to claim it before it bubbles.
//
// Views are painted first to last,
// so they should process interactions last to first,
// so views in front (on top) can claim the interaction first.
for (let i = subviews.length - 1; i >= 0; i--) {
const subview = subviews[i];
if (rectIntersectsRect(visibleArea, subview.visibleArea)) {
subview.handleInteractionAndPropagateToSubviews(interaction, viewRefs);
const didSubviewHandle =
subview.handleInteractionAndPropagateToSubviews(
interaction,
viewRefs,
) === true;
if (didSubviewHandle) {
return true;
}
}
});
}

const didSelfHandle =
this.handleInteraction(interaction, viewRefs) === true;
if (didSelfHandle) {
return true;
}

return false;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,11 @@

export * from './BackgroundColorView';
export * from './HorizontalPanAndZoomView';
export * from './ResizableView';
export * from './Surface';
export * from './VerticalScrollView';
export * from './View';
export * from './geometry';
export * from './layouter';
export * from './resizable';
export * from './useCanvasInteraction';
export * from './vertical-scroll-overflow';
Original file line number Diff line number Diff line change
Expand Up @@ -206,50 +206,6 @@ export const atLeastContainerHeightLayout: Layouter = (
}));
};

/**
* Forces last view to take up the space below the second-last view.
* Intended to be used with a vertical stack layout.
*/
export const lastViewTakesUpRemainingSpaceLayout: Layouter = (
layout,
containerFrame,
) => {
if (layout.length === 0) {
// Nothing to do
return layout;
}

if (layout.length === 1) {
// No second-last view; the view should just take up the container height
return containerHeightLayout(layout, containerFrame);
}

const layoutInfoToPassThrough = layout.slice(0, layout.length - 1);
const secondLastLayoutInfo =
layoutInfoToPassThrough[layoutInfoToPassThrough.length - 1];

const remainingHeight =
containerFrame.size.height -
secondLastLayoutInfo.frame.origin.y -
secondLastLayoutInfo.frame.size.height;
const height = Math.max(remainingHeight, 0); // Prevent negative heights

const lastLayoutInfo = layout[layout.length - 1];
return [
...layoutInfoToPassThrough,
{
...lastLayoutInfo,
frame: {
origin: lastLayoutInfo.frame.origin,
size: {
width: lastLayoutInfo.frame.size.width,
height,
},
},
},
];
};

/**
* Create a layouter that applies each layouter in `layouters` in sequence.
*/
Expand Down
Loading