@@ -18,54 +18,34 @@ import type {
18
18
WheelWithMetaInteraction ,
19
19
} from './useCanvasInteraction' ;
20
20
import type { Rect } from './geometry' ;
21
+ import type { ScrollState } from './utils/scrollState' ;
21
22
22
23
import { Surface } from './Surface' ;
23
24
import { View } from './View' ;
24
25
import { rectContainsPoint } from './geometry' ;
25
- import { clamp } from './utils/clamp' ;
26
26
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 ,
28
35
MAX_ZOOM_LEVEL ,
36
+ MIN_ZOOM_LEVEL ,
29
37
MOVE_WHEEL_DELTA_THRESHOLD ,
30
38
} from './constants' ;
31
39
32
- type HorizontalPanAndZoomState = $ReadOnly < { |
33
- /** Horizontal offset; positive in the left direction */
34
- offsetX : number ,
35
- zoomLevel : number ,
36
- | } > ;
37
-
38
40
export type HorizontalPanAndZoomViewOnChangeCallback = (
39
- state : HorizontalPanAndZoomState ,
41
+ state : ScrollState ,
40
42
view : HorizontalPanAndZoomView ,
41
43
) => void ;
42
44
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
-
59
45
export class HorizontalPanAndZoomView extends View {
60
46
_intrinsicContentWidth : number ;
61
-
62
- _panAndZoomState : HorizontalPanAndZoomState = {
63
- offsetX : 0 ,
64
- zoomLevel : 0.25 ,
65
- } ;
66
-
67
47
_isPanning = false ;
68
-
48
+ _scrollState : ScrollState = { offset : 0 , length : 0 } ;
69
49
_onStateChange : HorizontalPanAndZoomViewOnChangeCallback = ( ) => { } ;
70
50
71
51
constructor (
@@ -78,45 +58,52 @@ export class HorizontalPanAndZoomView extends View {
78
58
super ( surface , frame ) ;
79
59
this . addSubview ( contentView ) ;
80
60
this . _intrinsicContentWidth = intrinsicContentWidth ;
61
+ this . _setScrollState ( {
62
+ offset : 0 ,
63
+ length : intrinsicContentWidth * DEFAULT_ZOOM_LEVEL ,
64
+ } ) ;
81
65
if ( onStateChange ) this . _onStateChange = onStateChange ;
82
66
}
83
67
84
68
setFrame ( newFrame : Rect ) {
85
69
super . setFrame ( newFrame ) ;
86
70
87
- // Revalidate panAndZoomState
88
- this . _setStateAndInformCallbacksIfChanged ( this . _panAndZoomState ) ;
71
+ // Revalidate scrollState
72
+ this . _setStateAndInformCallbacksIfChanged ( this . _scrollState ) ;
89
73
}
90
74
91
- setPanAndZoomState ( proposedState : HorizontalPanAndZoomState ) {
92
- this . _setPanAndZoomState ( proposedState ) ;
75
+ setScrollState ( proposedState : ScrollState ) {
76
+ this . _setScrollState ( proposedState ) ;
93
77
}
94
78
95
79
/**
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.
98
82
*
99
83
* @returns Whether state was changed
100
84
* @private
101
85
*/
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 ) ) {
105
94
return false ;
106
95
}
107
- this . _panAndZoomState = clampedState ;
96
+ this . _scrollState = clampedState ;
108
97
this . setNeedsDisplay ( ) ;
109
98
return true ;
110
99
}
111
100
112
101
/**
113
102
* @private
114
103
*/
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 ) ;
120
107
}
121
108
}
122
109
@@ -133,17 +120,14 @@ export class HorizontalPanAndZoomView extends View {
133
120
}
134
121
135
122
layoutSubviews ( ) {
136
- const { offsetX , zoomLevel } = this . _panAndZoomState ;
123
+ const { offset , length } = this . _scrollState ;
137
124
const proposedFrame = {
138
125
origin : {
139
- x : this . frame . origin . x + offsetX ,
126
+ x : this . frame . origin . x + offset ,
140
127
y : this . frame . origin . y ,
141
128
} ,
142
129
size : {
143
- width : zoomLevelAndIntrinsicWidthToFrameWidth (
144
- zoomLevel ,
145
- this . _intrinsicContentWidth ,
146
- ) ,
130
+ width : length ,
147
131
height : this . frame . size . height ,
148
132
} ,
149
133
} ;
@@ -157,27 +141,18 @@ export class HorizontalPanAndZoomView extends View {
157
141
*
158
142
* Does not inform callbacks of state change since this is a public API.
159
143
*/
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 ,
179
154
} ) ;
180
- this . _setPanAndZoomState ( offsetAdjustedState ) ;
155
+ this . _setScrollState ( newState ) ;
181
156
}
182
157
183
158
_handleMouseDown ( interaction : MouseDownInteraction ) {
@@ -190,12 +165,12 @@ export class HorizontalPanAndZoomView extends View {
190
165
if ( ! this . _isPanning ) {
191
166
return ;
192
167
}
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 ,
198
172
} ) ;
173
+ this . _setStateAndInformCallbacksIfChanged ( newState ) ;
199
174
}
200
175
201
176
_handleMouseUp ( interaction : MouseUpInteraction ) {
@@ -209,6 +184,7 @@ export class HorizontalPanAndZoomView extends View {
209
184
location,
210
185
delta : { deltaX, deltaY} ,
211
186
} = interaction . payload ;
187
+
212
188
if ( ! rectContainsPoint ( location , this . frame ) ) {
213
189
return ; // Not scrolling on view
214
190
}
@@ -218,15 +194,16 @@ export class HorizontalPanAndZoomView extends View {
218
194
if ( absDeltaY > absDeltaX ) {
219
195
return ; // Scrolling vertically
220
196
}
221
-
222
197
if ( absDeltaX < MOVE_WHEEL_DELTA_THRESHOLD ) {
223
198
return ;
224
199
}
225
200
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 ,
229
205
} ) ;
206
+ this . _setStateAndInformCallbacksIfChanged ( newState ) ;
230
207
}
231
208
232
209
_handleWheelZoom (
@@ -239,6 +216,7 @@ export class HorizontalPanAndZoomView extends View {
239
216
location ,
240
217
delta : { deltaY } ,
241
218
} = interaction . payload ;
219
+
242
220
if ( ! rectContainsPoint ( location , this . frame ) ) {
243
221
return ; // Not scrolling on view
244
222
}
@@ -248,28 +226,16 @@ export class HorizontalPanAndZoomView extends View {
248
226
return ;
249
227
}
250
228
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 ,
266
233
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 ,
270
237
} ) ;
271
-
272
- this . _setStateAndInformCallbacksIfChanged ( offsetAdjustedState ) ;
238
+ this . _setStateAndInformCallbacksIfChanged ( newState ) ;
273
239
}
274
240
275
241
handleInteraction ( interaction : Interaction ) {
@@ -293,50 +259,4 @@ export class HorizontalPanAndZoomView extends View {
293
259
break ;
294
260
}
295
261
}
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
- }
342
262
}
0 commit comments