diff --git a/packages/react-devtools-scheduling-profiler/src/CanvasPage.js b/packages/react-devtools-scheduling-profiler/src/CanvasPage.js
index 470e39efac8f4..59939cd948692 100644
--- a/packages/react-devtools-scheduling-profiler/src/CanvasPage.js
+++ b/packages/react-devtools-scheduling-profiler/src/CanvasPage.js
@@ -7,14 +7,12 @@
* @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';
@@ -55,13 +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';
@@ -69,16 +68,22 @@ const CONTEXT_MENU_ID = 'canvas';
type Props = {|
profilerData: ReactProfilerData,
+ viewState: ViewState,
|};
-function CanvasPage({profilerData}: Props) {
+function CanvasPage({profilerData, viewState}: Props) {
return (
{({height, width}: {height: number, width: number}) => (
-
+
)}
@@ -103,23 +108,40 @@ const copySummary = (data: ReactProfilerData, measure: ReactMeasure) => {
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(null);
const [isContextMenuShown, setIsContextMenuShown] = useState(false);
@@ -137,9 +159,6 @@ function AutoSizedCanvas({data, height, width}: AutoSizedCanvasProps) {
const componentMeasuresViewRef = useRef(null);
const reactMeasuresViewRef = useRef(null);
const flamechartViewRef = useRef(null);
- const syncedHorizontalPanAndZoomViewsRef = useRef(
- [],
- );
const {hideMenu: hideContextMenu} = useContext(RegistryContext);
@@ -147,25 +166,24 @@ function AutoSizedCanvas({data, height, width}: AutoSizedCanvasProps) {
const surface = surfaceRef.current;
const defaultFrame = {origin: zeroPoint, size: {width, height}};
- // Clear synced views
- syncedHorizontalPanAndZoomViewsRef.current = [];
-
- const syncAllHorizontalPanAndZoomViewStates: HorizontalPanAndZoomViewOnChangeCallback = (
- newState,
- triggeringView?: HorizontalPanAndZoomView,
- ) => {
- // Hide context menu when panning.
+ // Auto hide context menu when panning.
+ viewState.onHorizontalScrollStateChange(scrollState => {
hideContextMenu();
+ });
- 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 {
@@ -175,6 +193,8 @@ function AutoSizedCanvas({data, height, width}: AutoSizedCanvasProps) {
surface,
defaultFrame,
view,
+ viewState,
+ label,
);
}
@@ -183,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(
@@ -207,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) {
@@ -218,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);
@@ -236,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) {
@@ -262,7 +287,7 @@ function AutoSizedCanvas({data, height, width}: AutoSizedCanvasProps) {
reactMeasuresViewRef.current = reactMeasuresView;
const reactMeasuresViewWrapper = createViewHelper(
reactMeasuresView,
- 'react',
+ 'react scheduling',
true,
true,
);
@@ -275,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(
@@ -335,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':
@@ -623,11 +651,7 @@ function AutoSizedCanvas({data, height, width}: AutoSizedCanvasProps) {
{measure !== null && (
- zoomToBatch(
- contextData.data,
- measure,
- syncedHorizontalPanAndZoomViewsRef.current,
- )
+ zoomToBatch(contextData.data, measure, viewState, width)
}
title="Zoom to batch">
Zoom to batch
diff --git a/packages/react-devtools-scheduling-profiler/src/SchedulingProfiler.js b/packages/react-devtools-scheduling-profiler/src/SchedulingProfiler.js
index 9b3220c5ae1b7..98cd61d312447 100644
--- a/packages/react-devtools-scheduling-profiler/src/SchedulingProfiler.js
+++ b/packages/react-devtools-scheduling-profiler/src/SchedulingProfiler.js
@@ -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}
/>
) : (
@@ -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 = ({
);
}
- return ;
+ return ;
};
diff --git a/packages/react-devtools-scheduling-profiler/src/SchedulingProfilerContext.js b/packages/react-devtools-scheduling-profiler/src/SchedulingProfilerContext.js
index dc95fc31a9da6..ff81482c09e74 100644
--- a/packages/react-devtools-scheduling-profiler/src/SchedulingProfilerContext.js
+++ b/packages/react-devtools-scheduling-profiler/src/SchedulingProfilerContext.js
@@ -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(
@@ -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(() => {
+ const horizontalScrollStateChangeCallbacks: Set = 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,
],
);
diff --git a/packages/react-devtools-scheduling-profiler/src/types.js b/packages/react-devtools-scheduling-profiler/src/types.js
index 0891e6c607b1d..761033f2cdda6 100644
--- a/packages/react-devtools-scheduling-profiler/src/types.js
+++ b/packages/react-devtools-scheduling-profiler/src/types.js
@@ -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,6 +123,22 @@ 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,
+|};
+
export type ReactProfilerData = {|
batchUIDToMeasuresMap: Map,
componentMeasures: ReactComponentMeasure[],
diff --git a/packages/react-devtools-scheduling-profiler/src/view-base/HorizontalPanAndZoomView.js b/packages/react-devtools-scheduling-profiler/src/view-base/HorizontalPanAndZoomView.js
index 62fca51be1ef5..5519599f12dcc 100644
--- a/packages/react-devtools-scheduling-profiler/src/view-base/HorizontalPanAndZoomView.js
+++ b/packages/react-devtools-scheduling-profiler/src/view-base/HorizontalPanAndZoomView.js
@@ -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,97 +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._intrinsicContentWidth = intrinsicContentWidth;
- this._setScrollState({
- offset: 0,
- length: intrinsicContentWidth * DEFAULT_ZOOM_LEVEL,
- });
-
- this._onStateChange = onStateChange;
- }
- setFrame(newFrame: Rect) {
- super.setFrame(newFrame);
+ this._contentView = contentView;
+ this._intrinsicContentWidth = intrinsicContentWidth;
+ this._viewState = viewState;
- // Revalidate scrollState
- this._setStateAndInformCallbacksIfChanged(this._scrollState);
- }
+ viewState.onHorizontalScrollStateChange(scrollState => {
+ this.zoomToRange(scrollState.offset, scrollState.length);
+ });
- 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,
@@ -135,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) {
@@ -184,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) {
@@ -227,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);
}
}
}
diff --git a/packages/react-devtools-scheduling-profiler/src/view-base/ResizableView.js b/packages/react-devtools-scheduling-profiler/src/view-base/ResizableView.js
index 625e71e24089b..1f0f890ce38ef 100644
--- a/packages/react-devtools-scheduling-profiler/src/view-base/ResizableView.js
+++ b/packages/react-devtools-scheduling-profiler/src/view-base/ResizableView.js
@@ -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;
}
diff --git a/packages/react-devtools-scheduling-profiler/src/view-base/VerticalScrollView.js b/packages/react-devtools-scheduling-profiler/src/view-base/VerticalScrollView.js
index 245b53aff7180..3f2f775912166 100644
--- a/packages/react-devtools-scheduling-profiler/src/view-base/VerticalScrollView.js
+++ b/packages/react-devtools-scheduling-profiler/src/view-base/VerticalScrollView.js
@@ -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;
-
- constructor(surface: Surface, frame: Rect, contentView: View) {
+ _contentView: View;
+ _isPanning: boolean;
+ _mutableViewStateKey: string;
+ _scrollState: ScrollState;
+ _viewState: ViewState;
+
+ 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();
}
}
diff --git a/packages/react-devtools-scheduling-profiler/src/view-base/utils/scrollState.js b/packages/react-devtools-scheduling-profiler/src/view-base/utils/scrollState.js
index e7f73a86d33fb..9e9bdb4919f3f 100644
--- a/packages/react-devtools-scheduling-profiler/src/view-base/utils/scrollState.js
+++ b/packages/react-devtools-scheduling-profiler/src/view-base/utils/scrollState.js
@@ -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 {