Skip to content

Commit e945489

Browse files
authored
Scheduling Profiler: Extract and test scroll state from horizontal pan and zoom view (facebook#19682)
* Extract reusable scroll logic from HorizontalPanAndZoomView * Change VerticalScrollView to use scrollState * Clarify test name
1 parent 9340083 commit e945489

File tree

7 files changed

+599
-197
lines changed

7 files changed

+599
-197
lines changed

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

+1-2
Original file line numberDiff line numberDiff line change
@@ -149,8 +149,7 @@ function AutoSizedCanvas({data, height, width}: AutoSizedCanvasProps) {
149149
) => {
150150
syncedHorizontalPanAndZoomViewsRef.current.forEach(
151151
syncedView =>
152-
triggeringView !== syncedView &&
153-
syncedView.setPanAndZoomState(newState),
152+
triggeringView !== syncedView && syncedView.setScrollState(newState),
154153
);
155154
};
156155

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

+68-148
Original file line numberDiff line numberDiff line change
@@ -18,54 +18,34 @@ import type {
1818
WheelWithMetaInteraction,
1919
} from './useCanvasInteraction';
2020
import type {Rect} from './geometry';
21+
import type {ScrollState} from './utils/scrollState';
2122

2223
import {Surface} from './Surface';
2324
import {View} from './View';
2425
import {rectContainsPoint} from './geometry';
25-
import {clamp} from './utils/clamp';
2626
import {
27-
MIN_ZOOM_LEVEL,
27+
clampState,
28+
moveStateToRange,
29+
areScrollStatesEqual,
30+
translateState,
31+
zoomState,
32+
} from './utils/scrollState';
33+
import {
34+
DEFAULT_ZOOM_LEVEL,
2835
MAX_ZOOM_LEVEL,
36+
MIN_ZOOM_LEVEL,
2937
MOVE_WHEEL_DELTA_THRESHOLD,
3038
} from './constants';
3139

32-
type HorizontalPanAndZoomState = $ReadOnly<{|
33-
/** Horizontal offset; positive in the left direction */
34-
offsetX: number,
35-
zoomLevel: number,
36-
|}>;
37-
3840
export type HorizontalPanAndZoomViewOnChangeCallback = (
39-
state: HorizontalPanAndZoomState,
41+
state: ScrollState,
4042
view: HorizontalPanAndZoomView,
4143
) => void;
4244

43-
function panAndZoomStatesAreEqual(
44-
state1: HorizontalPanAndZoomState,
45-
state2: HorizontalPanAndZoomState,
46-
): boolean {
47-
return (
48-
state1.offsetX === state2.offsetX && state1.zoomLevel === state2.zoomLevel
49-
);
50-
}
51-
52-
function zoomLevelAndIntrinsicWidthToFrameWidth(
53-
zoomLevel: number,
54-
intrinsicWidth: number,
55-
): number {
56-
return intrinsicWidth * zoomLevel;
57-
}
58-
5945
export class HorizontalPanAndZoomView extends View {
6046
_intrinsicContentWidth: number;
61-
62-
_panAndZoomState: HorizontalPanAndZoomState = {
63-
offsetX: 0,
64-
zoomLevel: 0.25,
65-
};
66-
6747
_isPanning = false;
68-
48+
_scrollState: ScrollState = {offset: 0, length: 0};
6949
_onStateChange: HorizontalPanAndZoomViewOnChangeCallback = () => {};
7050

7151
constructor(
@@ -78,45 +58,52 @@ export class HorizontalPanAndZoomView extends View {
7858
super(surface, frame);
7959
this.addSubview(contentView);
8060
this._intrinsicContentWidth = intrinsicContentWidth;
61+
this._setScrollState({
62+
offset: 0,
63+
length: intrinsicContentWidth * DEFAULT_ZOOM_LEVEL,
64+
});
8165
if (onStateChange) this._onStateChange = onStateChange;
8266
}
8367

8468
setFrame(newFrame: Rect) {
8569
super.setFrame(newFrame);
8670

87-
// Revalidate panAndZoomState
88-
this._setStateAndInformCallbacksIfChanged(this._panAndZoomState);
71+
// Revalidate scrollState
72+
this._setStateAndInformCallbacksIfChanged(this._scrollState);
8973
}
9074

91-
setPanAndZoomState(proposedState: HorizontalPanAndZoomState) {
92-
this._setPanAndZoomState(proposedState);
75+
setScrollState(proposedState: ScrollState) {
76+
this._setScrollState(proposedState);
9377
}
9478

9579
/**
96-
* Just sets pan and zoom state. Use `_setStateAndInformCallbacksIfChanged`
97-
* if this view's callbacks should also be called.
80+
* Just sets scroll state. Use `_setStateAndInformCallbacksIfChanged` if this
81+
* view's callbacks should also be called.
9882
*
9983
* @returns Whether state was changed
10084
* @private
10185
*/
102-
_setPanAndZoomState(proposedState: HorizontalPanAndZoomState): boolean {
103-
const clampedState = this._clampedProposedState(proposedState);
104-
if (panAndZoomStatesAreEqual(clampedState, this._panAndZoomState)) {
86+
_setScrollState(proposedState: ScrollState): boolean {
87+
const clampedState = clampState({
88+
state: proposedState,
89+
minContentLength: this._intrinsicContentWidth * MIN_ZOOM_LEVEL,
90+
maxContentLength: this._intrinsicContentWidth * MAX_ZOOM_LEVEL,
91+
containerLength: this.frame.size.width,
92+
});
93+
if (areScrollStatesEqual(clampedState, this._scrollState)) {
10594
return false;
10695
}
107-
this._panAndZoomState = clampedState;
96+
this._scrollState = clampedState;
10897
this.setNeedsDisplay();
10998
return true;
11099
}
111100

112101
/**
113102
* @private
114103
*/
115-
_setStateAndInformCallbacksIfChanged(
116-
proposedState: HorizontalPanAndZoomState,
117-
) {
118-
if (this._setPanAndZoomState(proposedState)) {
119-
this._onStateChange(this._panAndZoomState, this);
104+
_setStateAndInformCallbacksIfChanged(proposedState: ScrollState) {
105+
if (this._setScrollState(proposedState)) {
106+
this._onStateChange(this._scrollState, this);
120107
}
121108
}
122109

@@ -133,17 +120,14 @@ export class HorizontalPanAndZoomView extends View {
133120
}
134121

135122
layoutSubviews() {
136-
const {offsetX, zoomLevel} = this._panAndZoomState;
123+
const {offset, length} = this._scrollState;
137124
const proposedFrame = {
138125
origin: {
139-
x: this.frame.origin.x + offsetX,
126+
x: this.frame.origin.x + offset,
140127
y: this.frame.origin.y,
141128
},
142129
size: {
143-
width: zoomLevelAndIntrinsicWidthToFrameWidth(
144-
zoomLevel,
145-
this._intrinsicContentWidth,
146-
),
130+
width: length,
147131
height: this.frame.size.height,
148132
},
149133
};
@@ -157,27 +141,18 @@ export class HorizontalPanAndZoomView extends View {
157141
*
158142
* Does not inform callbacks of state change since this is a public API.
159143
*/
160-
zoomToRange(startX: number, endX: number) {
161-
// Zoom and offset must be done separately, so that if the zoom level is
162-
// clamped the offset will still be correct (unless it gets clamped too).
163-
const zoomClampedState = this._clampedProposedStateZoomLevel({
164-
...this._panAndZoomState,
165-
// Let:
166-
// I = intrinsic content width, i = zoom range = (endX - startX).
167-
// W = contentView's final zoomed width, w = this view's width
168-
// Goal: we want the visible width w to only contain the requested range i.
169-
// Derivation:
170-
// (1) i/I = w/W (by intuitive definition of variables)
171-
// (2) W = zoomLevel * I (definition of zoomLevel)
172-
// => zoomLevel = W/I (algebraic manipulation)
173-
// = w/i (rearranging (1))
174-
zoomLevel: this.frame.size.width / (endX - startX),
175-
});
176-
const offsetAdjustedState = this._clampedProposedStateOffsetX({
177-
...zoomClampedState,
178-
offsetX: -startX * zoomClampedState.zoomLevel,
144+
zoomToRange(rangeStart: number, rangeEnd: number) {
145+
const newState = moveStateToRange({
146+
state: this._scrollState,
147+
rangeStart,
148+
rangeEnd,
149+
contentLength: this._intrinsicContentWidth,
150+
151+
minContentLength: this._intrinsicContentWidth * MIN_ZOOM_LEVEL,
152+
maxContentLength: this._intrinsicContentWidth * MAX_ZOOM_LEVEL,
153+
containerLength: this.frame.size.width,
179154
});
180-
this._setPanAndZoomState(offsetAdjustedState);
155+
this._setScrollState(newState);
181156
}
182157

183158
_handleMouseDown(interaction: MouseDownInteraction) {
@@ -190,12 +165,12 @@ export class HorizontalPanAndZoomView extends View {
190165
if (!this._isPanning) {
191166
return;
192167
}
193-
const {offsetX} = this._panAndZoomState;
194-
const {movementX} = interaction.payload.event;
195-
this._setStateAndInformCallbacksIfChanged({
196-
...this._panAndZoomState,
197-
offsetX: offsetX + movementX,
168+
const newState = translateState({
169+
state: this._scrollState,
170+
delta: interaction.payload.event.movementX,
171+
containerLength: this.frame.size.width,
198172
});
173+
this._setStateAndInformCallbacksIfChanged(newState);
199174
}
200175

201176
_handleMouseUp(interaction: MouseUpInteraction) {
@@ -209,6 +184,7 @@ export class HorizontalPanAndZoomView extends View {
209184
location,
210185
delta: {deltaX, deltaY},
211186
} = interaction.payload;
187+
212188
if (!rectContainsPoint(location, this.frame)) {
213189
return; // Not scrolling on view
214190
}
@@ -218,15 +194,16 @@ export class HorizontalPanAndZoomView extends View {
218194
if (absDeltaY > absDeltaX) {
219195
return; // Scrolling vertically
220196
}
221-
222197
if (absDeltaX < MOVE_WHEEL_DELTA_THRESHOLD) {
223198
return;
224199
}
225200

226-
this._setStateAndInformCallbacksIfChanged({
227-
...this._panAndZoomState,
228-
offsetX: this._panAndZoomState.offsetX - deltaX,
201+
const newState = translateState({
202+
state: this._scrollState,
203+
delta: -deltaX,
204+
containerLength: this.frame.size.width,
229205
});
206+
this._setStateAndInformCallbacksIfChanged(newState);
230207
}
231208

232209
_handleWheelZoom(
@@ -239,6 +216,7 @@ export class HorizontalPanAndZoomView extends View {
239216
location,
240217
delta: {deltaY},
241218
} = interaction.payload;
219+
242220
if (!rectContainsPoint(location, this.frame)) {
243221
return; // Not scrolling on view
244222
}
@@ -248,28 +226,16 @@ export class HorizontalPanAndZoomView extends View {
248226
return;
249227
}
250228

251-
const zoomClampedState = this._clampedProposedStateZoomLevel({
252-
...this._panAndZoomState,
253-
zoomLevel: this._panAndZoomState.zoomLevel * (1 + 0.005 * -deltaY),
254-
});
255-
256-
// Determine where the mouse is, and adjust the offset so that point stays
257-
// centered after zooming.
258-
const oldMouseXInFrame = location.x - zoomClampedState.offsetX;
259-
const fractionalMouseX =
260-
oldMouseXInFrame / this._contentView.frame.size.width;
261-
const newContentWidth = zoomLevelAndIntrinsicWidthToFrameWidth(
262-
zoomClampedState.zoomLevel,
263-
this._intrinsicContentWidth,
264-
);
265-
const newMouseXInFrame = fractionalMouseX * newContentWidth;
229+
const newState = zoomState({
230+
state: this._scrollState,
231+
multiplier: 1 + 0.005 * -deltaY,
232+
fixedPoint: location.x - this._scrollState.offset,
266233

267-
const offsetAdjustedState = this._clampedProposedStateOffsetX({
268-
...zoomClampedState,
269-
offsetX: location.x - newMouseXInFrame,
234+
minContentLength: this._intrinsicContentWidth * MIN_ZOOM_LEVEL,
235+
maxContentLength: this._intrinsicContentWidth * MAX_ZOOM_LEVEL,
236+
containerLength: this.frame.size.width,
270237
});
271-
272-
this._setStateAndInformCallbacksIfChanged(offsetAdjustedState);
238+
this._setStateAndInformCallbacksIfChanged(newState);
273239
}
274240

275241
handleInteraction(interaction: Interaction) {
@@ -293,50 +259,4 @@ export class HorizontalPanAndZoomView extends View {
293259
break;
294260
}
295261
}
296-
297-
/**
298-
* @private
299-
*/
300-
_clampedProposedStateZoomLevel(
301-
proposedState: HorizontalPanAndZoomState,
302-
): HorizontalPanAndZoomState {
303-
// Content-based min zoom level to ensure that contentView's width >= our width.
304-
const minContentBasedZoomLevel =
305-
this.frame.size.width / this._intrinsicContentWidth;
306-
const minZoomLevel = Math.max(MIN_ZOOM_LEVEL, minContentBasedZoomLevel);
307-
return {
308-
...proposedState,
309-
zoomLevel: clamp(minZoomLevel, MAX_ZOOM_LEVEL, proposedState.zoomLevel),
310-
};
311-
}
312-
313-
/**
314-
* @private
315-
*/
316-
_clampedProposedStateOffsetX(
317-
proposedState: HorizontalPanAndZoomState,
318-
): HorizontalPanAndZoomState {
319-
const newContentWidth = zoomLevelAndIntrinsicWidthToFrameWidth(
320-
proposedState.zoomLevel,
321-
this._intrinsicContentWidth,
322-
);
323-
return {
324-
...proposedState,
325-
offsetX: clamp(
326-
-(newContentWidth - this.frame.size.width),
327-
0,
328-
proposedState.offsetX,
329-
),
330-
};
331-
}
332-
333-
/**
334-
* @private
335-
*/
336-
_clampedProposedState(
337-
proposedState: HorizontalPanAndZoomState,
338-
): HorizontalPanAndZoomState {
339-
const zoomClampedState = this._clampedProposedStateZoomLevel(proposedState);
340-
return this._clampedProposedStateOffsetX(zoomClampedState);
341-
}
342262
}

0 commit comments

Comments
 (0)