mirror of
https://github.com/hanzoai/react.git
synced 2026-09-24 17:02:27 +00:00
Scheduling Profiler: Misc UX and performance improvements (#22043)
This commit is contained in:
+78
-48
@@ -7,19 +7,18 @@
|
||||
* @flow
|
||||
*/
|
||||
|
||||
import type {
|
||||
Point,
|
||||
HorizontalPanAndZoomViewOnChangeCallback,
|
||||
} from './view-base';
|
||||
import type {Point} from './view-base';
|
||||
import type {
|
||||
ReactHoverContextInfo,
|
||||
ReactProfilerData,
|
||||
ReactMeasure,
|
||||
ViewState,
|
||||
} from './types';
|
||||
|
||||
import * as React from 'react';
|
||||
import {
|
||||
Fragment,
|
||||
useContext,
|
||||
useEffect,
|
||||
useLayoutEffect,
|
||||
useRef,
|
||||
@@ -54,12 +53,14 @@ import {
|
||||
UserTimingMarksView,
|
||||
} from './content-views';
|
||||
import {COLORS} from './content-views/constants';
|
||||
|
||||
import {clampState, moveStateToRange} from './view-base/utils/scrollState';
|
||||
import EventTooltip from './EventTooltip';
|
||||
import {RegistryContext} from 'react-devtools-shared/src/devtools/ContextMenu/Contexts';
|
||||
import ContextMenu from 'react-devtools-shared/src/devtools/ContextMenu/ContextMenu';
|
||||
import ContextMenuItem from 'react-devtools-shared/src/devtools/ContextMenu/ContextMenuItem';
|
||||
import useContextMenu from 'react-devtools-shared/src/devtools/ContextMenu/useContextMenu';
|
||||
import {getBatchRange} from './utils/getBatchRange';
|
||||
import {MAX_ZOOM_LEVEL, MIN_ZOOM_LEVEL} from './view-base/constants';
|
||||
|
||||
import styles from './CanvasPage.css';
|
||||
|
||||
@@ -67,16 +68,22 @@ const CONTEXT_MENU_ID = 'canvas';
|
||||
|
||||
type Props = {|
|
||||
profilerData: ReactProfilerData,
|
||||
viewState: ViewState,
|
||||
|};
|
||||
|
||||
function CanvasPage({profilerData}: Props) {
|
||||
function CanvasPage({profilerData, viewState}: Props) {
|
||||
return (
|
||||
<div
|
||||
className={styles.CanvasPage}
|
||||
style={{backgroundColor: COLORS.BACKGROUND}}>
|
||||
<AutoSizer>
|
||||
{({height, width}: {height: number, width: number}) => (
|
||||
<AutoSizedCanvas data={profilerData} height={height} width={width} />
|
||||
<AutoSizedCanvas
|
||||
data={profilerData}
|
||||
height={height}
|
||||
viewState={viewState}
|
||||
width={width}
|
||||
/>
|
||||
)}
|
||||
</AutoSizer>
|
||||
</div>
|
||||
@@ -98,27 +105,43 @@ const copySummary = (data: ReactProfilerData, measure: ReactMeasure) => {
|
||||
);
|
||||
};
|
||||
|
||||
// TODO (scheduling profiler) Why is the "zoom" feature so much slower than normal rendering?
|
||||
const zoomToBatch = (
|
||||
data: ReactProfilerData,
|
||||
measure: ReactMeasure,
|
||||
syncedHorizontalPanAndZoomViews: HorizontalPanAndZoomView[],
|
||||
viewState: ViewState,
|
||||
width: number,
|
||||
) => {
|
||||
const {batchUID} = measure;
|
||||
const [startTime, stopTime] = getBatchRange(batchUID, data);
|
||||
syncedHorizontalPanAndZoomViews.forEach(syncedView =>
|
||||
// Using time as range works because the views' intrinsic content size is based on time.
|
||||
syncedView.zoomToRange(startTime, stopTime),
|
||||
);
|
||||
const [rangeStart, rangeEnd] = getBatchRange(batchUID, data);
|
||||
|
||||
// Convert from time range to ScrollState
|
||||
const scrollState = moveStateToRange({
|
||||
state: viewState.horizontalScrollState,
|
||||
rangeStart,
|
||||
rangeEnd,
|
||||
contentLength: data.duration,
|
||||
|
||||
minContentLength: data.duration * MIN_ZOOM_LEVEL,
|
||||
maxContentLength: data.duration * MAX_ZOOM_LEVEL,
|
||||
containerLength: width,
|
||||
});
|
||||
|
||||
viewState.updateHorizontalScrollState(scrollState);
|
||||
};
|
||||
|
||||
type AutoSizedCanvasProps = {|
|
||||
data: ReactProfilerData,
|
||||
height: number,
|
||||
viewState: ViewState,
|
||||
width: number,
|
||||
|};
|
||||
|
||||
function AutoSizedCanvas({data, height, width}: AutoSizedCanvasProps) {
|
||||
function AutoSizedCanvas({
|
||||
data,
|
||||
height,
|
||||
viewState,
|
||||
width,
|
||||
}: AutoSizedCanvasProps) {
|
||||
const canvasRef = useRef<HTMLCanvasElement | null>(null);
|
||||
|
||||
const [isContextMenuShown, setIsContextMenuShown] = useState<boolean>(false);
|
||||
@@ -136,30 +159,31 @@ function AutoSizedCanvas({data, height, width}: AutoSizedCanvasProps) {
|
||||
const componentMeasuresViewRef = useRef(null);
|
||||
const reactMeasuresViewRef = useRef(null);
|
||||
const flamechartViewRef = useRef(null);
|
||||
const syncedHorizontalPanAndZoomViewsRef = useRef<HorizontalPanAndZoomView[]>(
|
||||
[],
|
||||
);
|
||||
|
||||
const {hideMenu: hideContextMenu} = useContext(RegistryContext);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const surface = surfaceRef.current;
|
||||
const defaultFrame = {origin: zeroPoint, size: {width, height}};
|
||||
|
||||
// Clear synced views
|
||||
syncedHorizontalPanAndZoomViewsRef.current = [];
|
||||
// Auto hide context menu when panning.
|
||||
viewState.onHorizontalScrollStateChange(scrollState => {
|
||||
hideContextMenu();
|
||||
});
|
||||
|
||||
const syncAllHorizontalPanAndZoomViewStates: HorizontalPanAndZoomViewOnChangeCallback = (
|
||||
newState,
|
||||
triggeringView?: HorizontalPanAndZoomView,
|
||||
) => {
|
||||
syncedHorizontalPanAndZoomViewsRef.current.forEach(
|
||||
syncedView =>
|
||||
triggeringView !== syncedView && syncedView.setScrollState(newState),
|
||||
);
|
||||
};
|
||||
// Initialize horizontal view state
|
||||
viewState.updateHorizontalScrollState(
|
||||
clampState({
|
||||
state: viewState.horizontalScrollState,
|
||||
minContentLength: data.duration * MIN_ZOOM_LEVEL,
|
||||
maxContentLength: data.duration * MAX_ZOOM_LEVEL,
|
||||
containerLength: defaultFrame.size.width,
|
||||
}),
|
||||
);
|
||||
|
||||
function createViewHelper(
|
||||
view: View,
|
||||
resizeLabel: string = '',
|
||||
label: string,
|
||||
shouldScrollVertically: boolean = false,
|
||||
shouldResizeVertically: boolean = false,
|
||||
): View {
|
||||
@@ -169,6 +193,8 @@ function AutoSizedCanvas({data, height, width}: AutoSizedCanvasProps) {
|
||||
surface,
|
||||
defaultFrame,
|
||||
view,
|
||||
viewState,
|
||||
label,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -177,23 +203,22 @@ function AutoSizedCanvas({data, height, width}: AutoSizedCanvasProps) {
|
||||
defaultFrame,
|
||||
verticalScrollView !== null ? verticalScrollView : view,
|
||||
data.duration,
|
||||
syncAllHorizontalPanAndZoomViewStates,
|
||||
viewState,
|
||||
);
|
||||
|
||||
syncedHorizontalPanAndZoomViewsRef.current.push(horizontalPanAndZoomView);
|
||||
|
||||
let viewToReturn = horizontalPanAndZoomView;
|
||||
let resizableView = null;
|
||||
if (shouldResizeVertically) {
|
||||
viewToReturn = new ResizableView(
|
||||
resizableView = new ResizableView(
|
||||
surface,
|
||||
defaultFrame,
|
||||
horizontalPanAndZoomView,
|
||||
viewState,
|
||||
canvasRef,
|
||||
resizeLabel,
|
||||
label,
|
||||
);
|
||||
}
|
||||
|
||||
return viewToReturn;
|
||||
return resizableView || horizontalPanAndZoomView;
|
||||
}
|
||||
|
||||
const axisMarkersView = new TimeAxisMarkersView(
|
||||
@@ -201,7 +226,7 @@ function AutoSizedCanvas({data, height, width}: AutoSizedCanvasProps) {
|
||||
defaultFrame,
|
||||
data.duration,
|
||||
);
|
||||
const axisMarkersViewWrapper = createViewHelper(axisMarkersView);
|
||||
const axisMarkersViewWrapper = createViewHelper(axisMarkersView, 'time');
|
||||
|
||||
let userTimingMarksViewWrapper = null;
|
||||
if (data.otherUserTimingMarks.length > 0) {
|
||||
@@ -212,7 +237,10 @@ function AutoSizedCanvas({data, height, width}: AutoSizedCanvasProps) {
|
||||
data.duration,
|
||||
);
|
||||
userTimingMarksViewRef.current = userTimingMarksView;
|
||||
userTimingMarksViewWrapper = createViewHelper(userTimingMarksView);
|
||||
userTimingMarksViewWrapper = createViewHelper(
|
||||
userTimingMarksView,
|
||||
'user timing api',
|
||||
);
|
||||
}
|
||||
|
||||
const nativeEventsView = new NativeEventsView(surface, defaultFrame, data);
|
||||
@@ -230,7 +258,10 @@ function AutoSizedCanvas({data, height, width}: AutoSizedCanvasProps) {
|
||||
data,
|
||||
);
|
||||
schedulingEventsViewRef.current = schedulingEventsView;
|
||||
const schedulingEventsViewWrapper = createViewHelper(schedulingEventsView);
|
||||
const schedulingEventsViewWrapper = createViewHelper(
|
||||
schedulingEventsView,
|
||||
'react updates',
|
||||
);
|
||||
|
||||
let suspenseEventsViewWrapper = null;
|
||||
if (data.suspenseEvents.length > 0) {
|
||||
@@ -256,7 +287,7 @@ function AutoSizedCanvas({data, height, width}: AutoSizedCanvasProps) {
|
||||
reactMeasuresViewRef.current = reactMeasuresView;
|
||||
const reactMeasuresViewWrapper = createViewHelper(
|
||||
reactMeasuresView,
|
||||
'react',
|
||||
'react scheduling',
|
||||
true,
|
||||
true,
|
||||
);
|
||||
@@ -269,7 +300,10 @@ function AutoSizedCanvas({data, height, width}: AutoSizedCanvasProps) {
|
||||
data,
|
||||
);
|
||||
componentMeasuresViewRef.current = componentMeasuresView;
|
||||
componentMeasuresViewWrapper = createViewHelper(componentMeasuresView);
|
||||
componentMeasuresViewWrapper = createViewHelper(
|
||||
componentMeasuresView,
|
||||
'react components',
|
||||
);
|
||||
}
|
||||
|
||||
const flamechartView = new FlamechartView(
|
||||
@@ -329,7 +363,7 @@ function AutoSizedCanvas({data, height, width}: AutoSizedCanvasProps) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Wheel events should always hide the current toolltip.
|
||||
// Wheel events should always hide the current tooltip.
|
||||
switch (interaction.type) {
|
||||
case 'wheel-control':
|
||||
case 'wheel-meta':
|
||||
@@ -617,11 +651,7 @@ function AutoSizedCanvas({data, height, width}: AutoSizedCanvasProps) {
|
||||
{measure !== null && (
|
||||
<ContextMenuItem
|
||||
onClick={() =>
|
||||
zoomToBatch(
|
||||
contextData.data,
|
||||
measure,
|
||||
syncedHorizontalPanAndZoomViewsRef.current,
|
||||
)
|
||||
zoomToBatch(contextData.data, measure, viewState, width)
|
||||
}
|
||||
title="Zoom to batch">
|
||||
Zoom to batch
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
*/
|
||||
|
||||
import type {DataResource} from './createDataResourceFromImportedFile';
|
||||
import type {ViewState} from './types';
|
||||
|
||||
import * as React from 'react';
|
||||
import {
|
||||
@@ -27,9 +28,11 @@ import CanvasPage from './CanvasPage';
|
||||
import styles from './SchedulingProfiler.css';
|
||||
|
||||
export function SchedulingProfiler(_: {||}) {
|
||||
const {importSchedulingProfilerData, schedulingProfilerData} = useContext(
|
||||
SchedulingProfilerContext,
|
||||
);
|
||||
const {
|
||||
importSchedulingProfilerData,
|
||||
schedulingProfilerData,
|
||||
viewState,
|
||||
} = useContext(SchedulingProfilerContext);
|
||||
|
||||
const ref = useRef(null);
|
||||
|
||||
@@ -66,6 +69,7 @@ export function SchedulingProfiler(_: {||}) {
|
||||
dataResource={schedulingProfilerData}
|
||||
key={key}
|
||||
onFileSelect={importSchedulingProfilerData}
|
||||
viewState={viewState}
|
||||
/>
|
||||
</Suspense>
|
||||
) : (
|
||||
@@ -130,9 +134,11 @@ const CouldNotLoadProfile = ({error, onFileSelect}) => (
|
||||
const DataResourceComponent = ({
|
||||
dataResource,
|
||||
onFileSelect,
|
||||
viewState,
|
||||
}: {|
|
||||
dataResource: DataResource,
|
||||
onFileSelect: (file: File) => void,
|
||||
viewState: ViewState,
|
||||
|}) => {
|
||||
const dataOrError = dataResource.read();
|
||||
if (dataOrError instanceof Error) {
|
||||
@@ -140,5 +146,5 @@ const DataResourceComponent = ({
|
||||
<CouldNotLoadProfile error={dataOrError} onFileSelect={onFileSelect} />
|
||||
);
|
||||
}
|
||||
return <CanvasPage profilerData={dataOrError} />;
|
||||
return <CanvasPage profilerData={dataOrError} viewState={viewState} />;
|
||||
};
|
||||
|
||||
+36
-3
@@ -11,12 +11,14 @@ import * as React from 'react';
|
||||
import {createContext, useCallback, useMemo, useState} from 'react';
|
||||
import createDataResourceFromImportedFile from './createDataResourceFromImportedFile';
|
||||
|
||||
import type {HorizontalScrollStateChangeCallback, ViewState} from './types';
|
||||
import type {DataResource} from './createDataResourceFromImportedFile';
|
||||
|
||||
export type Context = {|
|
||||
clearSchedulingProfilerData: () => void,
|
||||
importSchedulingProfilerData: (file: File) => void,
|
||||
schedulingProfilerData: DataResource | null,
|
||||
viewState: ViewState,
|
||||
|};
|
||||
|
||||
const SchedulingProfilerContext = createContext<Context>(
|
||||
@@ -42,20 +44,51 @@ function SchedulingProfilerContextController({children}: Props) {
|
||||
setSchedulingProfilerData(createDataResourceFromImportedFile(file));
|
||||
}, []);
|
||||
|
||||
// TODO (scheduling profiler) Start/stop time ref here?
|
||||
// Recreate view state any time new profiling data is imported.
|
||||
const viewState = useMemo<ViewState>(() => {
|
||||
const horizontalScrollStateChangeCallbacks: Set<HorizontalScrollStateChangeCallback> = new Set();
|
||||
|
||||
const horizontalScrollState = {
|
||||
offset: 0,
|
||||
length: 0,
|
||||
};
|
||||
|
||||
return {
|
||||
horizontalScrollState,
|
||||
onHorizontalScrollStateChange: callback => {
|
||||
horizontalScrollStateChangeCallbacks.add(callback);
|
||||
},
|
||||
updateHorizontalScrollState: scrollState => {
|
||||
if (
|
||||
horizontalScrollState.offset === scrollState.offset &&
|
||||
horizontalScrollState.length === scrollState.length
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
horizontalScrollState.offset = scrollState.offset;
|
||||
horizontalScrollState.length = scrollState.length;
|
||||
|
||||
horizontalScrollStateChangeCallbacks.forEach(callback => {
|
||||
callback(scrollState);
|
||||
});
|
||||
},
|
||||
viewToMutableViewStateMap: new Map(),
|
||||
};
|
||||
}, [schedulingProfilerData]);
|
||||
|
||||
const value = useMemo(
|
||||
() => ({
|
||||
clearSchedulingProfilerData,
|
||||
importSchedulingProfilerData,
|
||||
schedulingProfilerData,
|
||||
// TODO (scheduling profiler)
|
||||
viewState,
|
||||
}),
|
||||
[
|
||||
clearSchedulingProfilerData,
|
||||
importSchedulingProfilerData,
|
||||
schedulingProfilerData,
|
||||
// TODO (scheduling profiler)
|
||||
viewState,
|
||||
],
|
||||
);
|
||||
|
||||
|
||||
+9
-21
@@ -38,21 +38,12 @@ import {REACT_TOTAL_NUM_LANES} from '../constants';
|
||||
const REACT_LANE_HEIGHT = REACT_MEASURE_HEIGHT + BORDER_SIZE;
|
||||
const MAX_ROWS_TO_SHOW_INITIALLY = 5;
|
||||
|
||||
function getMeasuresForLane(
|
||||
allMeasures: ReactMeasure[],
|
||||
lane: ReactLane,
|
||||
): ReactMeasure[] {
|
||||
return allMeasures.filter(measure => measure.lanes.includes(lane));
|
||||
}
|
||||
|
||||
export class ReactMeasuresView extends View {
|
||||
_profilerData: ReactProfilerData;
|
||||
_intrinsicSize: IntrinsicSize;
|
||||
|
||||
_lanesToRender: ReactLane[];
|
||||
_laneToMeasures: Map<ReactLane, ReactMeasure[]>;
|
||||
|
||||
_profilerData: ReactProfilerData;
|
||||
_hoveredMeasure: ReactMeasure | null = null;
|
||||
|
||||
onHover: ((measure: ReactMeasure | null) => void) | null = null;
|
||||
|
||||
constructor(surface: Surface, frame: Rect, profilerData: ReactProfilerData) {
|
||||
@@ -63,17 +54,14 @@ export class ReactMeasuresView extends View {
|
||||
|
||||
_performPreflightComputations() {
|
||||
this._lanesToRender = [];
|
||||
this._laneToMeasures = new Map();
|
||||
|
||||
for (let lane: ReactLane = 0; lane < REACT_TOTAL_NUM_LANES; lane++) {
|
||||
const measuresForLane = getMeasuresForLane(
|
||||
this._profilerData.measures,
|
||||
const measuresForLane = this._profilerData.laneToReactMeasureMap.get(
|
||||
lane,
|
||||
);
|
||||
// Only show lanes with measures
|
||||
if (measuresForLane.length) {
|
||||
if (measuresForLane != null && measuresForLane.length > 0) {
|
||||
this._lanesToRender.push(lane);
|
||||
this._laneToMeasures.set(lane, measuresForLane);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -213,7 +201,7 @@ export class ReactMeasuresView extends View {
|
||||
frame,
|
||||
_hoveredMeasure,
|
||||
_lanesToRender,
|
||||
_laneToMeasures,
|
||||
_profilerData,
|
||||
visibleArea,
|
||||
} = this;
|
||||
|
||||
@@ -233,7 +221,7 @@ export class ReactMeasuresView extends View {
|
||||
for (let i = 0; i < _lanesToRender.length; i++) {
|
||||
const lane = _lanesToRender[i];
|
||||
const baseY = frame.origin.y + i * REACT_LANE_HEIGHT;
|
||||
const measuresForLane = _laneToMeasures.get(lane);
|
||||
const measuresForLane = _profilerData.laneToReactMeasureMap.get(lane);
|
||||
|
||||
if (!measuresForLane) {
|
||||
throw new Error(
|
||||
@@ -242,7 +230,7 @@ export class ReactMeasuresView extends View {
|
||||
}
|
||||
|
||||
// Render lane labels
|
||||
const label = this._profilerData.laneToLabelMap.get(lane);
|
||||
const label = _profilerData.laneToLabelMap.get(lane);
|
||||
if (label == null) {
|
||||
console.warn(`Could not find label for lane ${lane}.`);
|
||||
} else {
|
||||
@@ -316,8 +304,8 @@ export class ReactMeasuresView extends View {
|
||||
frame,
|
||||
_intrinsicSize,
|
||||
_lanesToRender,
|
||||
_laneToMeasures,
|
||||
onHover,
|
||||
_profilerData,
|
||||
visibleArea,
|
||||
} = this;
|
||||
if (!onHover) {
|
||||
@@ -347,7 +335,7 @@ export class ReactMeasuresView extends View {
|
||||
// This will always be the one on "top" (the one the user is hovering over).
|
||||
const scaleFactor = positioningScaleFactor(_intrinsicSize.width, frame);
|
||||
const hoverTimestamp = positionToTimestamp(location.x, scaleFactor, frame);
|
||||
const measures = _laneToMeasures.get(lane);
|
||||
const measures = _profilerData.laneToReactMeasureMap.get(lane);
|
||||
if (!measures) {
|
||||
onHover(null);
|
||||
return;
|
||||
|
||||
+692
-369
File diff suppressed because it is too large
Load Diff
+42
-16
@@ -13,13 +13,14 @@ import {
|
||||
} from '@elg/speedscope';
|
||||
import type {TimelineEvent} from '@elg/speedscope';
|
||||
import type {
|
||||
Milliseconds,
|
||||
BatchUID,
|
||||
Flamechart,
|
||||
Milliseconds,
|
||||
NativeEvent,
|
||||
Phase,
|
||||
ReactLane,
|
||||
ReactComponentMeasure,
|
||||
ReactMeasure,
|
||||
ReactMeasureType,
|
||||
ReactProfilerData,
|
||||
SchedulingEvent,
|
||||
@@ -33,7 +34,7 @@ import {getBatchRange} from '../utils/getBatchRange';
|
||||
type MeasureStackElement = {|
|
||||
type: ReactMeasureType,
|
||||
depth: number,
|
||||
index: number,
|
||||
measure: ReactMeasure,
|
||||
startTime: Milliseconds,
|
||||
stopTime?: Milliseconds,
|
||||
|};
|
||||
@@ -92,17 +93,19 @@ export function getLanesFromTransportDecimalBitmask(
|
||||
return lanes;
|
||||
}
|
||||
|
||||
const laneToLabelMap: Map<number, string> = new Map();
|
||||
function updateLaneToLabelMap(laneLabelTuplesString: string): void {
|
||||
function updateLaneToLabelMap(
|
||||
profilerData: ReactProfilerData,
|
||||
laneLabelTuplesString: string,
|
||||
): void {
|
||||
// These marks appear multiple times in the data;
|
||||
// We only need to extact them once.
|
||||
if (laneToLabelMap.size === 0) {
|
||||
if (profilerData.laneToLabelMap.size === 0) {
|
||||
const laneLabelTuples = laneLabelTuplesString.split(',');
|
||||
for (let laneIndex = 0; laneIndex < laneLabelTuples.length; laneIndex++) {
|
||||
// The numeric lane value (e.g. 64) isn't important.
|
||||
// The profiler parses and stores the lane's position within the bitmap,
|
||||
// (e.g. lane 1 is index 0, lane 16 is index 4).
|
||||
laneToLabelMap.set(laneIndex, laneLabelTuples[laneIndex]);
|
||||
profilerData.laneToLabelMap.set(laneIndex, laneLabelTuples[laneIndex]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -133,18 +136,32 @@ function markWorkStarted(
|
||||
state: ProcessorState,
|
||||
) {
|
||||
const {batchUID, measureStack} = state;
|
||||
const index = currentProfilerData.measures.length;
|
||||
const depth = getDepth(measureStack);
|
||||
|
||||
state.measureStack.push({depth, index, startTime, type});
|
||||
|
||||
currentProfilerData.measures.push({
|
||||
const measure: ReactMeasure = {
|
||||
type,
|
||||
batchUID,
|
||||
depth,
|
||||
lanes,
|
||||
timestamp: startTime,
|
||||
duration: 0,
|
||||
};
|
||||
|
||||
state.measureStack.push({depth, measure, startTime, type});
|
||||
|
||||
// This array is pre-initialized when the batchUID is generated.
|
||||
const measures = currentProfilerData.batchUIDToMeasuresMap.get(batchUID);
|
||||
if (measures != null) {
|
||||
measures.push(measure);
|
||||
} else {
|
||||
currentProfilerData.batchUIDToMeasuresMap.set(state.batchUID, [measure]);
|
||||
}
|
||||
|
||||
// This array is pre-initialized before processing starts.
|
||||
lanes.forEach(lane => {
|
||||
((currentProfilerData.laneToReactMeasureMap.get(
|
||||
lane,
|
||||
): any): ReactMeasure[]).push(measure);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -174,8 +191,7 @@ function markWorkCompleted(
|
||||
);
|
||||
}
|
||||
|
||||
const {index, startTime} = stack.pop();
|
||||
const measure = currentProfilerData.measures[index];
|
||||
const {measure, startTime} = stack.pop();
|
||||
if (!measure) {
|
||||
console.error('Could not find matching measure for type "%s".', type);
|
||||
}
|
||||
@@ -282,7 +298,7 @@ function processTimelineEvent(
|
||||
}
|
||||
} else if (name.startsWith('--react-lane-labels-')) {
|
||||
const [laneLabelTuplesString] = name.substr(20).split('-');
|
||||
updateLaneToLabelMap(laneLabelTuplesString);
|
||||
updateLaneToLabelMap(currentProfilerData, laneLabelTuplesString);
|
||||
} else if (name.startsWith('--component-render-start-')) {
|
||||
const [componentName] = name.substr(25).split('-');
|
||||
|
||||
@@ -648,12 +664,18 @@ export default function preprocessData(
|
||||
): ReactProfilerData {
|
||||
const flamechart = preprocessFlamechart(timeline);
|
||||
|
||||
const laneToReactMeasureMap = new Map();
|
||||
for (let lane: ReactLane = 0; lane < REACT_TOTAL_NUM_LANES; lane++) {
|
||||
laneToReactMeasureMap.set(lane, []);
|
||||
}
|
||||
|
||||
const profilerData: ReactProfilerData = {
|
||||
batchUIDToMeasuresMap: new Map(),
|
||||
componentMeasures: [],
|
||||
duration: 0,
|
||||
flamechart,
|
||||
laneToLabelMap,
|
||||
measures: [],
|
||||
laneToLabelMap: new Map(),
|
||||
laneToReactMeasureMap,
|
||||
nativeEvents: [],
|
||||
otherUserTimingMarks: [],
|
||||
reactVersion: null,
|
||||
@@ -739,7 +761,11 @@ export default function preprocessData(
|
||||
state.potentialSuspenseEventsOutsideOfTransition.forEach(
|
||||
([suspenseEvent, lanes]) => {
|
||||
// HACK This is a bit gross but the numeric lane value might change between render versions.
|
||||
if (!lanes.some(lane => laneToLabelMap.get(lane) === 'Transition')) {
|
||||
if (
|
||||
!lanes.some(
|
||||
lane => profilerData.laneToLabelMap.get(lane) === 'Transition',
|
||||
)
|
||||
) {
|
||||
suspenseEvent.warning = WARNING_STRINGS.SUSPEND_DURING_UPATE;
|
||||
}
|
||||
},
|
||||
|
||||
+19
-2
@@ -7,7 +7,7 @@
|
||||
* @flow
|
||||
*/
|
||||
|
||||
// Type utilities
|
||||
import type {ScrollState} from './view-base/utils/scrollState';
|
||||
|
||||
// Source: https://github.com/facebook/flow/issues/4002#issuecomment-323612798
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
@@ -123,12 +123,29 @@ export type FlamechartStackLayer = FlamechartStackFrame[];
|
||||
|
||||
export type Flamechart = FlamechartStackLayer[];
|
||||
|
||||
export type HorizontalScrollStateChangeCallback = (
|
||||
scrollState: ScrollState,
|
||||
) => void;
|
||||
|
||||
// Imperative view state that corresponds to profiler data.
|
||||
// This state lives outside of React's lifecycle
|
||||
// and should be erased/reset whenever new profiler data is loaded.
|
||||
export type ViewState = {|
|
||||
horizontalScrollState: ScrollState,
|
||||
onHorizontalScrollStateChange: (
|
||||
callback: HorizontalScrollStateChangeCallback,
|
||||
) => void,
|
||||
updateHorizontalScrollState: (scrollState: ScrollState) => void,
|
||||
viewToMutableViewStateMap: Map<string, mixed>,
|
||||
|};
|
||||
|
||||
export type ReactProfilerData = {|
|
||||
batchUIDToMeasuresMap: Map<BatchUID, ReactMeasure[]>,
|
||||
componentMeasures: ReactComponentMeasure[],
|
||||
duration: number,
|
||||
flamechart: Flamechart,
|
||||
laneToLabelMap: Map<ReactLane, string>,
|
||||
measures: ReactMeasure[],
|
||||
laneToReactMeasureMap: Map<ReactLane, ReactMeasure[]>,
|
||||
nativeEvents: NativeEvent[],
|
||||
otherUserTimingMarks: UserTimingMark[],
|
||||
reactVersion: string | null,
|
||||
|
||||
@@ -9,37 +9,35 @@
|
||||
|
||||
import memoize from 'memoize-one';
|
||||
|
||||
import type {BatchUID, Milliseconds, ReactProfilerData} from '../types';
|
||||
import type {
|
||||
BatchUID,
|
||||
Milliseconds,
|
||||
ReactMeasure,
|
||||
ReactProfilerData,
|
||||
} from '../types';
|
||||
|
||||
function unmemoizedGetBatchRange(
|
||||
batchUID: BatchUID,
|
||||
data: ReactProfilerData,
|
||||
minStartTime?: ?number,
|
||||
minStartTime?: number = 0,
|
||||
): [Milliseconds, Milliseconds] {
|
||||
const {measures} = data;
|
||||
|
||||
let startTime = 0;
|
||||
let stopTime = Infinity;
|
||||
|
||||
let i = 0;
|
||||
|
||||
// Find the first measure in the current batch.
|
||||
for (i; i < measures.length; i++) {
|
||||
const measure = measures[i];
|
||||
if (measure.batchUID === batchUID) {
|
||||
if (minStartTime == null || measure.timestamp >= minStartTime) {
|
||||
startTime = measure.timestamp;
|
||||
break;
|
||||
}
|
||||
}
|
||||
const measures = data.batchUIDToMeasuresMap.get(batchUID);
|
||||
if (measures == null || measures.length === 0) {
|
||||
throw Error(`Could not find measures with batch UID "${batchUID}"`);
|
||||
}
|
||||
|
||||
// Find the last measure in the current batch.
|
||||
for (i; i < measures.length; i++) {
|
||||
const measure = measures[i];
|
||||
if (measure.batchUID === batchUID) {
|
||||
stopTime = measure.timestamp;
|
||||
} else {
|
||||
const lastMeasure = ((measures[measures.length - 1]: any): ReactMeasure);
|
||||
const stopTime = lastMeasure.timestamp + lastMeasure.duration;
|
||||
|
||||
if (stopTime < minStartTime) {
|
||||
return [0, 0];
|
||||
}
|
||||
|
||||
let startTime = minStartTime;
|
||||
for (let index = 0; index < measures.length; index++) {
|
||||
const measure = measures[index];
|
||||
if (measure.timestamp >= minStartTime) {
|
||||
startTime = measure.timestamp;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
+58
-89
@@ -18,6 +18,7 @@ import type {
|
||||
import type {Rect} from './geometry';
|
||||
import type {ScrollState} from './utils/scrollState';
|
||||
import type {ViewRefs} from './Surface';
|
||||
import type {ViewState} from '../types';
|
||||
|
||||
import {Surface} from './Surface';
|
||||
import {View} from './View';
|
||||
@@ -30,96 +31,84 @@ import {
|
||||
zoomState,
|
||||
} from './utils/scrollState';
|
||||
import {
|
||||
DEFAULT_ZOOM_LEVEL,
|
||||
MAX_ZOOM_LEVEL,
|
||||
MIN_ZOOM_LEVEL,
|
||||
MOVE_WHEEL_DELTA_THRESHOLD,
|
||||
} from './constants';
|
||||
|
||||
export type HorizontalPanAndZoomViewOnChangeCallback = (
|
||||
state: ScrollState,
|
||||
view: HorizontalPanAndZoomView,
|
||||
) => void;
|
||||
|
||||
export class HorizontalPanAndZoomView extends View {
|
||||
_contentView: View;
|
||||
_intrinsicContentWidth: number;
|
||||
_isPanning = false;
|
||||
_scrollState: ScrollState = {offset: 0, length: 0};
|
||||
_onStateChange: HorizontalPanAndZoomViewOnChangeCallback = () => {};
|
||||
_viewState: ViewState;
|
||||
|
||||
constructor(
|
||||
surface: Surface,
|
||||
frame: Rect,
|
||||
contentView: View,
|
||||
intrinsicContentWidth: number,
|
||||
onStateChange?: HorizontalPanAndZoomViewOnChangeCallback,
|
||||
viewState: ViewState,
|
||||
) {
|
||||
super(surface, frame);
|
||||
this.addSubview(contentView);
|
||||
|
||||
this._contentView = contentView;
|
||||
this._intrinsicContentWidth = intrinsicContentWidth;
|
||||
this._setScrollState({
|
||||
offset: 0,
|
||||
length: intrinsicContentWidth * DEFAULT_ZOOM_LEVEL,
|
||||
this._viewState = viewState;
|
||||
|
||||
viewState.onHorizontalScrollStateChange(scrollState => {
|
||||
this.zoomToRange(scrollState.offset, scrollState.length);
|
||||
});
|
||||
if (onStateChange) this._onStateChange = onStateChange;
|
||||
}
|
||||
|
||||
setFrame(newFrame: Rect) {
|
||||
super.setFrame(newFrame);
|
||||
|
||||
// Revalidate scrollState
|
||||
this._setStateAndInformCallbacksIfChanged(this._scrollState);
|
||||
}
|
||||
|
||||
setScrollState(proposedState: ScrollState) {
|
||||
this._setScrollState(proposedState);
|
||||
this.addSubview(contentView);
|
||||
}
|
||||
|
||||
/**
|
||||
* Just sets scroll state. Use `_setStateAndInformCallbacksIfChanged` if this
|
||||
* view's callbacks should also be called.
|
||||
* Just sets scroll state.
|
||||
* Use `_setStateAndInformCallbacksIfChanged` if this view's callbacks should also be called.
|
||||
*
|
||||
* @returns Whether state was changed
|
||||
* @private
|
||||
*/
|
||||
_setScrollState(proposedState: ScrollState): boolean {
|
||||
setScrollState(proposedState: ScrollState) {
|
||||
const clampedState = clampState({
|
||||
state: proposedState,
|
||||
minContentLength: this._intrinsicContentWidth * MIN_ZOOM_LEVEL,
|
||||
maxContentLength: this._intrinsicContentWidth * MAX_ZOOM_LEVEL,
|
||||
containerLength: this.frame.size.width,
|
||||
});
|
||||
if (areScrollStatesEqual(clampedState, this._scrollState)) {
|
||||
return false;
|
||||
if (
|
||||
!areScrollStatesEqual(clampedState, this._viewState.horizontalScrollState)
|
||||
) {
|
||||
this.setNeedsDisplay();
|
||||
}
|
||||
this._scrollState = clampedState;
|
||||
this.setNeedsDisplay();
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* Zoom to a specific range of the content specified as a range of the
|
||||
* content view's intrinsic content size.
|
||||
*
|
||||
* Does not inform callbacks of state change since this is a public API.
|
||||
*/
|
||||
_setStateAndInformCallbacksIfChanged(proposedState: ScrollState) {
|
||||
if (this._setScrollState(proposedState)) {
|
||||
this._onStateChange(this._scrollState, this);
|
||||
}
|
||||
zoomToRange(rangeStart: number, rangeEnd: number) {
|
||||
const newState = moveStateToRange({
|
||||
state: this._viewState.horizontalScrollState,
|
||||
rangeStart,
|
||||
rangeEnd,
|
||||
contentLength: this._intrinsicContentWidth,
|
||||
|
||||
minContentLength: this._intrinsicContentWidth * MIN_ZOOM_LEVEL,
|
||||
maxContentLength: this._intrinsicContentWidth * MAX_ZOOM_LEVEL,
|
||||
containerLength: this.frame.size.width,
|
||||
});
|
||||
this.setScrollState(newState);
|
||||
}
|
||||
|
||||
desiredSize() {
|
||||
return this._contentView.desiredSize();
|
||||
}
|
||||
|
||||
/**
|
||||
* Reference to the content view. This view is also the only view in
|
||||
* `this.subviews`.
|
||||
*/
|
||||
get _contentView() {
|
||||
return this.subviews[0];
|
||||
}
|
||||
|
||||
layoutSubviews() {
|
||||
const {offset, length} = this._scrollState;
|
||||
const {offset, length} = this._viewState.horizontalScrollState;
|
||||
const proposedFrame = {
|
||||
origin: {
|
||||
x: this.frame.origin.x + offset,
|
||||
@@ -134,24 +123,22 @@ export class HorizontalPanAndZoomView extends View {
|
||||
super.layoutSubviews();
|
||||
}
|
||||
|
||||
/**
|
||||
* Zoom to a specific range of the content specified as a range of the
|
||||
* content view's intrinsic content size.
|
||||
*
|
||||
* Does not inform callbacks of state change since this is a public API.
|
||||
*/
|
||||
zoomToRange(rangeStart: number, rangeEnd: number) {
|
||||
const newState = moveStateToRange({
|
||||
state: this._scrollState,
|
||||
rangeStart,
|
||||
rangeEnd,
|
||||
contentLength: this._intrinsicContentWidth,
|
||||
|
||||
minContentLength: this._intrinsicContentWidth * MIN_ZOOM_LEVEL,
|
||||
maxContentLength: this._intrinsicContentWidth * MAX_ZOOM_LEVEL,
|
||||
containerLength: this.frame.size.width,
|
||||
});
|
||||
this._setScrollState(newState);
|
||||
handleInteraction(interaction: Interaction, viewRefs: ViewRefs) {
|
||||
switch (interaction.type) {
|
||||
case 'mousedown':
|
||||
this._handleMouseDown(interaction, viewRefs);
|
||||
break;
|
||||
case 'mousemove':
|
||||
this._handleMouseMove(interaction, viewRefs);
|
||||
break;
|
||||
case 'mouseup':
|
||||
this._handleMouseUp(interaction, viewRefs);
|
||||
break;
|
||||
case 'wheel-plain':
|
||||
case 'wheel-shift':
|
||||
this._handleWheel(interaction);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
_handleMouseDown(interaction: MouseDownInteraction, viewRefs: ViewRefs) {
|
||||
@@ -183,11 +170,11 @@ export class HorizontalPanAndZoomView extends View {
|
||||
return;
|
||||
}
|
||||
const newState = translateState({
|
||||
state: this._scrollState,
|
||||
state: this._viewState.horizontalScrollState,
|
||||
delta: interaction.payload.event.movementX,
|
||||
containerLength: this.frame.size.width,
|
||||
});
|
||||
this._setStateAndInformCallbacksIfChanged(newState);
|
||||
this._viewState.updateHorizontalScrollState(newState);
|
||||
}
|
||||
|
||||
_handleMouseUp(interaction: MouseUpInteraction, viewRefs: ViewRefs) {
|
||||
@@ -226,44 +213,26 @@ export class HorizontalPanAndZoomView extends View {
|
||||
}
|
||||
|
||||
const newState = zoomState({
|
||||
state: this._scrollState,
|
||||
state: this._viewState.horizontalScrollState,
|
||||
multiplier: 1 + 0.005 * -deltaY,
|
||||
fixedPoint: location.x - this._scrollState.offset,
|
||||
fixedPoint: location.x - this._viewState.horizontalScrollState.offset,
|
||||
|
||||
minContentLength: this._intrinsicContentWidth * MIN_ZOOM_LEVEL,
|
||||
maxContentLength: this._intrinsicContentWidth * MAX_ZOOM_LEVEL,
|
||||
containerLength: this.frame.size.width,
|
||||
});
|
||||
this._setStateAndInformCallbacksIfChanged(newState);
|
||||
this._viewState.updateHorizontalScrollState(newState);
|
||||
} else {
|
||||
if (absDeltaX < MOVE_WHEEL_DELTA_THRESHOLD) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newState = translateState({
|
||||
state: this._scrollState,
|
||||
state: this._viewState.horizontalScrollState,
|
||||
delta: -deltaX,
|
||||
containerLength: this.frame.size.width,
|
||||
});
|
||||
this._setStateAndInformCallbacksIfChanged(newState);
|
||||
}
|
||||
}
|
||||
|
||||
handleInteraction(interaction: Interaction, viewRefs: ViewRefs) {
|
||||
switch (interaction.type) {
|
||||
case 'mousedown':
|
||||
this._handleMouseDown(interaction, viewRefs);
|
||||
break;
|
||||
case 'mousemove':
|
||||
this._handleMouseMove(interaction, viewRefs);
|
||||
break;
|
||||
case 'mouseup':
|
||||
this._handleMouseUp(interaction, viewRefs);
|
||||
break;
|
||||
case 'wheel-plain':
|
||||
case 'wheel-shift':
|
||||
this._handleWheel(interaction);
|
||||
break;
|
||||
this._viewState.updateHorizontalScrollState(newState);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+48
-16
@@ -17,6 +17,7 @@ import type {
|
||||
} from './useCanvasInteraction';
|
||||
import type {Rect} from './geometry';
|
||||
import type {ViewRefs} from './Surface';
|
||||
import type {ViewState} from '../types';
|
||||
|
||||
import {BORDER_SIZE, COLORS} from '../content-views/constants';
|
||||
import {drawText} from '../content-views/utils/text';
|
||||
@@ -35,10 +36,10 @@ type ResizingState = $ReadOnly<{|
|
||||
mouseY: number,
|
||||
|}>;
|
||||
|
||||
type LayoutState = $ReadOnly<{|
|
||||
type LayoutState = {|
|
||||
/** Resize bar's vertical position relative to resize view's frame.origin.y */
|
||||
barOffsetY: number,
|
||||
|}>;
|
||||
|};
|
||||
|
||||
const RESIZE_BAR_DOT_RADIUS = 1;
|
||||
const RESIZE_BAR_DOT_SPACING = 4;
|
||||
@@ -217,36 +218,33 @@ class ResizeBar extends View {
|
||||
export class ResizableView extends View {
|
||||
_canvasRef: {current: HTMLCanvasElement | null};
|
||||
_layoutState: LayoutState;
|
||||
_mutableViewStateKey: string;
|
||||
_resizeBar: ResizeBar;
|
||||
_resizingState: ResizingState | null = null;
|
||||
_subview: View;
|
||||
_viewState: ViewState;
|
||||
|
||||
constructor(
|
||||
surface: Surface,
|
||||
frame: Rect,
|
||||
subview: View,
|
||||
viewState: ViewState,
|
||||
canvasRef: {current: HTMLCanvasElement | null},
|
||||
label: string,
|
||||
) {
|
||||
super(surface, frame, noopLayout);
|
||||
|
||||
this._canvasRef = canvasRef;
|
||||
|
||||
this._layoutState = {barOffsetY: 0};
|
||||
this._mutableViewStateKey = label + ':ResizableView';
|
||||
this._subview = subview;
|
||||
this._resizeBar = new ResizeBar(surface, frame, label);
|
||||
this._viewState = viewState;
|
||||
|
||||
this.addSubview(this._subview);
|
||||
this.addSubview(this._resizeBar);
|
||||
|
||||
const subviewDesiredSize = subview.desiredSize();
|
||||
this._updateLayoutStateAndResizeBar(
|
||||
subviewDesiredSize.maxInitialHeight != null
|
||||
? Math.min(
|
||||
subviewDesiredSize.maxInitialHeight,
|
||||
subviewDesiredSize.height,
|
||||
)
|
||||
: subviewDesiredSize.height,
|
||||
);
|
||||
this._restoreMutableViewState();
|
||||
}
|
||||
|
||||
desiredSize() {
|
||||
@@ -274,6 +272,35 @@ export class ResizableView extends View {
|
||||
super.layoutSubviews();
|
||||
}
|
||||
|
||||
_restoreMutableViewState() {
|
||||
if (
|
||||
this._viewState.viewToMutableViewStateMap.has(this._mutableViewStateKey)
|
||||
) {
|
||||
this._layoutState = ((this._viewState.viewToMutableViewStateMap.get(
|
||||
this._mutableViewStateKey,
|
||||
): any): LayoutState);
|
||||
|
||||
this._updateLayoutStateAndResizeBar(this._layoutState.barOffsetY);
|
||||
} else {
|
||||
this._viewState.viewToMutableViewStateMap.set(
|
||||
this._mutableViewStateKey,
|
||||
this._layoutState,
|
||||
);
|
||||
|
||||
const subviewDesiredSize = this._subview.desiredSize();
|
||||
this._updateLayoutStateAndResizeBar(
|
||||
subviewDesiredSize.maxInitialHeight != null
|
||||
? Math.min(
|
||||
subviewDesiredSize.maxInitialHeight,
|
||||
subviewDesiredSize.height,
|
||||
)
|
||||
: subviewDesiredSize.height,
|
||||
);
|
||||
}
|
||||
|
||||
this.setNeedsDisplay();
|
||||
}
|
||||
|
||||
_shouldRenderResizeBar() {
|
||||
const subviewDesiredSize = this._subview.desiredSize();
|
||||
return subviewDesiredSize.hideScrollBarIfLessThanHeight != null
|
||||
@@ -287,10 +314,7 @@ export class ResizableView extends View {
|
||||
barOffsetY = 0;
|
||||
}
|
||||
|
||||
this._layoutState = {
|
||||
...this._layoutState,
|
||||
barOffsetY,
|
||||
};
|
||||
this._layoutState.barOffsetY = barOffsetY;
|
||||
|
||||
this._resizeBar.showLabel = barOffsetY === 0;
|
||||
}
|
||||
@@ -341,6 +365,10 @@ export class ResizableView extends View {
|
||||
}
|
||||
|
||||
_handleClick(interaction: ClickInteraction) {
|
||||
if (!this._shouldRenderResizeBar()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const cursorInView = rectContainsPoint(
|
||||
interaction.payload.location,
|
||||
this.frame,
|
||||
@@ -356,6 +384,10 @@ export class ResizableView extends View {
|
||||
}
|
||||
|
||||
_handleDoubleClick(interaction: DoubleClickInteraction) {
|
||||
if (!this._shouldRenderResizeBar()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const cursorInView = rectContainsPoint(
|
||||
interaction.payload.location,
|
||||
this.frame,
|
||||
|
||||
+61
-30
@@ -17,6 +17,7 @@ import type {
|
||||
import type {Rect} from './geometry';
|
||||
import type {ScrollState} from './utils/scrollState';
|
||||
import type {ViewRefs} from './Surface';
|
||||
import type {ViewState} from '../types';
|
||||
|
||||
import {Surface} from './Surface';
|
||||
import {View} from './View';
|
||||
@@ -34,13 +35,33 @@ const CARET_WIDTH = 5;
|
||||
const CARET_HEIGHT = 3;
|
||||
|
||||
export class VerticalScrollView extends View {
|
||||
_scrollState: ScrollState = {offset: 0, length: 0};
|
||||
_isPanning = false;
|
||||
_contentView: View;
|
||||
_isPanning: boolean;
|
||||
_mutableViewStateKey: string;
|
||||
_scrollState: ScrollState;
|
||||
_viewState: ViewState;
|
||||
|
||||
constructor(surface: Surface, frame: Rect, contentView: View) {
|
||||
constructor(
|
||||
surface: Surface,
|
||||
frame: Rect,
|
||||
contentView: View,
|
||||
viewState: ViewState,
|
||||
label: string,
|
||||
) {
|
||||
super(surface, frame);
|
||||
|
||||
this._contentView = contentView;
|
||||
this._isPanning = false;
|
||||
this._mutableViewStateKey = label + ':VerticalScrollView';
|
||||
this._scrollState = {
|
||||
offset: 0,
|
||||
length: 0,
|
||||
};
|
||||
this._viewState = viewState;
|
||||
|
||||
this.addSubview(contentView);
|
||||
this._setScrollState(this._scrollState);
|
||||
|
||||
this._restoreMutableViewState();
|
||||
}
|
||||
|
||||
setFrame(newFrame: Rect) {
|
||||
@@ -102,14 +123,6 @@ export class VerticalScrollView extends View {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reference to the content view. This view is also the only view in
|
||||
* `this.subviews`.
|
||||
*/
|
||||
get _contentView() {
|
||||
return this.subviews[0];
|
||||
}
|
||||
|
||||
layoutSubviews() {
|
||||
const {offset} = this._scrollState;
|
||||
const desiredSize = this._contentView.desiredSize();
|
||||
@@ -133,6 +146,23 @@ export class VerticalScrollView extends View {
|
||||
super.layoutSubviews();
|
||||
}
|
||||
|
||||
handleInteraction(interaction: Interaction) {
|
||||
switch (interaction.type) {
|
||||
case 'mousedown':
|
||||
this._handleMouseDown(interaction);
|
||||
break;
|
||||
case 'mousemove':
|
||||
this._handleMouseMove(interaction);
|
||||
break;
|
||||
case 'mouseup':
|
||||
this._handleMouseUp(interaction);
|
||||
break;
|
||||
case 'wheel-shift':
|
||||
this._handleWheelShift(interaction);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
_handleMouseDown(interaction: MouseDownInteraction) {
|
||||
if (rectContainsPoint(interaction.payload.location, this.frame)) {
|
||||
this._isPanning = true;
|
||||
@@ -184,21 +214,21 @@ export class VerticalScrollView extends View {
|
||||
this._setScrollState(newState);
|
||||
}
|
||||
|
||||
handleInteraction(interaction: Interaction) {
|
||||
switch (interaction.type) {
|
||||
case 'mousedown':
|
||||
this._handleMouseDown(interaction);
|
||||
break;
|
||||
case 'mousemove':
|
||||
this._handleMouseMove(interaction);
|
||||
break;
|
||||
case 'mouseup':
|
||||
this._handleMouseUp(interaction);
|
||||
break;
|
||||
case 'wheel-shift':
|
||||
this._handleWheelShift(interaction);
|
||||
break;
|
||||
_restoreMutableViewState() {
|
||||
if (
|
||||
this._viewState.viewToMutableViewStateMap.has(this._mutableViewStateKey)
|
||||
) {
|
||||
this._scrollState = ((this._viewState.viewToMutableViewStateMap.get(
|
||||
this._mutableViewStateKey,
|
||||
): any): ScrollState);
|
||||
} else {
|
||||
this._viewState.viewToMutableViewStateMap.set(
|
||||
this._mutableViewStateKey,
|
||||
this._scrollState,
|
||||
);
|
||||
}
|
||||
|
||||
this.setNeedsDisplay();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -212,10 +242,11 @@ export class VerticalScrollView extends View {
|
||||
maxContentLength: height,
|
||||
containerLength: this.frame.size.height,
|
||||
});
|
||||
if (areScrollStatesEqual(clampedState, this._scrollState)) {
|
||||
return;
|
||||
if (!areScrollStatesEqual(clampedState, this._scrollState)) {
|
||||
this._scrollState.offset = clampedState.offset;
|
||||
this._scrollState.length = clampedState.length;
|
||||
|
||||
this.setNeedsDisplay();
|
||||
}
|
||||
this._scrollState = clampedState;
|
||||
this.setNeedsDisplay();
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -18,10 +18,10 @@ import {clamp} from './clamp';
|
||||
* |<-------------------length------------------->|
|
||||
* ```
|
||||
*/
|
||||
export type ScrollState = $ReadOnly<{|
|
||||
export type ScrollState = {|
|
||||
offset: number,
|
||||
length: number,
|
||||
|}>;
|
||||
|};
|
||||
|
||||
function clampOffset(state: ScrollState, containerLength: number): ScrollState {
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user