@@ -18,54 +18,34 @@ import type {
1818 WheelWithMetaInteraction ,
1919} from './useCanvasInteraction' ;
2020import type { Rect } from './geometry' ;
21+ import type { ScrollState } from './utils/scrollState' ;
2122
2223import { Surface } from './Surface' ;
2324import { View } from './View' ;
2425import { rectContainsPoint } from './geometry' ;
25- import { clamp } from './utils/clamp' ;
2626import {
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-
3840export 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-
5945export 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