@@ -34,16 +34,15 @@ import {RoomPermalinkCreator} from "../../../utils/permalinks/Permalinks"
34
34
import { MatrixEvent } from "matrix-js-sdk/src/models/event" ;
35
35
import { normalizeWheelEvent } from "../../../utils/Mouse" ;
36
36
37
- const MIN_ZOOM = 100 ;
38
- const MAX_ZOOM = 300 ;
37
+ // Max scale to keep gaps around the image
38
+ const MAX_SCALE = 0.95 ;
39
39
// This is used for the buttons
40
- const ZOOM_STEP = 10 ;
40
+ const ZOOM_STEP = 0. 10;
41
41
// This is used for mouse wheel events
42
- const ZOOM_COEFFICIENT = 0.5 ;
42
+ const ZOOM_COEFFICIENT = 0.0025 ;
43
43
// If we have moved only this much we can zoom
44
44
const ZOOM_DISTANCE = 10 ;
45
45
46
-
47
46
interface IProps {
48
47
src : string , // the source of the image being displayed
49
48
name ?: string , // the main title ('name') for the image
@@ -62,8 +61,10 @@ interface IProps {
62
61
}
63
62
64
63
interface IState {
65
- rotation : number ,
66
64
zoom : number ,
65
+ minZoom : number ,
66
+ maxZoom : number ,
67
+ rotation : number ,
67
68
translationX : number ,
68
69
translationY : number ,
69
70
moving : boolean ,
@@ -75,8 +76,10 @@ export default class ImageView extends React.Component<IProps, IState> {
75
76
constructor ( props ) {
76
77
super ( props ) ;
77
78
this . state = {
79
+ zoom : 0 ,
80
+ minZoom : MAX_SCALE ,
81
+ maxZoom : MAX_SCALE ,
78
82
rotation : 0 ,
79
- zoom : MIN_ZOOM ,
80
83
translationX : 0 ,
81
84
translationY : 0 ,
82
85
moving : false ,
@@ -87,6 +90,8 @@ export default class ImageView extends React.Component<IProps, IState> {
87
90
// XXX: Refs to functional components
88
91
private contextMenuButton = createRef < any > ( ) ;
89
92
private focusLock = createRef < any > ( ) ;
93
+ private imageWrapper = createRef < HTMLDivElement > ( ) ;
94
+ private image = createRef < HTMLImageElement > ( ) ;
90
95
91
96
private initX = 0 ;
92
97
private initY = 0 ;
@@ -99,43 +104,93 @@ export default class ImageView extends React.Component<IProps, IState> {
99
104
// We have to use addEventListener() because the listener
100
105
// needs to be passive in order to work with Chromium
101
106
this . focusLock . current . addEventListener ( 'wheel' , this . onWheel , { passive : false } ) ;
107
+ // We want to recalculate zoom whenever the window's size changes
108
+ window . addEventListener ( "resize" , this . calculateZoom ) ;
109
+ // After the image loads for the first time we want to calculate the zoom
110
+ this . image . current . addEventListener ( "load" , this . calculateZoom ) ;
111
+ // Try to precalculate the zoom from width and height props
112
+ this . calculateZoom ( ) ;
102
113
}
103
114
104
115
componentWillUnmount ( ) {
105
116
this . focusLock . current . removeEventListener ( 'wheel' , this . onWheel ) ;
106
117
}
107
118
108
- private onKeyDown = ( ev : KeyboardEvent ) => {
109
- if ( ev . key === Key . ESCAPE ) {
110
- ev . stopPropagation ( ) ;
111
- ev . preventDefault ( ) ;
112
- this . props . onFinished ( ) ;
119
+ private calculateZoom = ( ) => {
120
+ const image = this . image . current ;
121
+ const imageWrapper = this . imageWrapper . current ;
122
+
123
+ const width = this . props . width || image . naturalWidth ;
124
+ const height = this . props . height || image . naturalHeight ;
125
+
126
+ const zoomX = imageWrapper . clientWidth / width ;
127
+ const zoomY = imageWrapper . clientHeight / height ;
128
+
129
+ // If the image is smaller in both dimensions set its the zoom to 1 to
130
+ // display it in its original size
131
+ if ( zoomX >= 1 && zoomY >= 1 ) {
132
+ this . setState ( {
133
+ zoom : 1 ,
134
+ minZoom : 1 ,
135
+ maxZoom : 1 ,
136
+ } ) ;
137
+ return ;
113
138
}
114
- } ;
139
+ // We set minZoom to the min of the zoomX and zoomY to avoid overflow in
140
+ // any direction. We also multiply by MAX_SCALE to get a gap around the
141
+ // image by default
142
+ const minZoom = Math . min ( zoomX , zoomY ) * MAX_SCALE ;
115
143
116
- private onWheel = ( ev : WheelEvent ) => {
117
- ev . stopPropagation ( ) ;
118
- ev . preventDefault ( ) ;
144
+ if ( this . state . zoom <= this . state . minZoom ) this . setState ( { zoom : minZoom } ) ;
145
+ this . setState ( {
146
+ minZoom : minZoom ,
147
+ maxZoom : 1 ,
148
+ } ) ;
149
+ }
119
150
120
- const { deltaY } = normalizeWheelEvent ( ev ) ;
121
- const newZoom = this . state . zoom - ( deltaY * ZOOM_COEFFICIENT ) ;
151
+ private zoom ( delta : number ) {
152
+ const newZoom = this . state . zoom + delta ;
122
153
123
- if ( newZoom <= MIN_ZOOM ) {
154
+ if ( newZoom <= this . state . minZoom ) {
124
155
this . setState ( {
125
- zoom : MIN_ZOOM ,
156
+ zoom : this . state . minZoom ,
126
157
translationX : 0 ,
127
158
translationY : 0 ,
128
159
} ) ;
129
160
return ;
130
161
}
131
- if ( newZoom >= MAX_ZOOM ) {
132
- this . setState ( { zoom : MAX_ZOOM } ) ;
162
+ if ( newZoom >= this . state . maxZoom ) {
163
+ this . setState ( { zoom : this . state . maxZoom } ) ;
133
164
return ;
134
165
}
135
166
136
167
this . setState ( {
137
168
zoom : newZoom ,
138
169
} ) ;
170
+ }
171
+
172
+ private onWheel = ( ev : WheelEvent ) => {
173
+ ev . stopPropagation ( ) ;
174
+ ev . preventDefault ( ) ;
175
+
176
+ const { deltaY} = normalizeWheelEvent ( ev ) ;
177
+ this . zoom ( - ( deltaY * ZOOM_COEFFICIENT ) ) ;
178
+ } ;
179
+
180
+ private onZoomInClick = ( ) => {
181
+ this . zoom ( ZOOM_STEP ) ;
182
+ } ;
183
+
184
+ private onZoomOutClick = ( ) => {
185
+ this . zoom ( - ZOOM_STEP ) ;
186
+ } ;
187
+
188
+ private onKeyDown = ( ev : KeyboardEvent ) => {
189
+ if ( ev . key === Key . ESCAPE ) {
190
+ ev . stopPropagation ( ) ;
191
+ ev . preventDefault ( ) ;
192
+ this . props . onFinished ( ) ;
193
+ }
139
194
} ;
140
195
141
196
private onRotateCounterClockwiseClick = ( ) => {
@@ -150,31 +205,6 @@ export default class ImageView extends React.Component<IProps, IState> {
150
205
this . setState ( { rotation : rotationDegrees } ) ;
151
206
} ;
152
207
153
- private onZoomInClick = ( ) => {
154
- if ( this . state . zoom >= MAX_ZOOM ) {
155
- this . setState ( { zoom : MAX_ZOOM } ) ;
156
- return ;
157
- }
158
-
159
- this . setState ( {
160
- zoom : this . state . zoom + ZOOM_STEP ,
161
- } ) ;
162
- } ;
163
-
164
- private onZoomOutClick = ( ) => {
165
- if ( this . state . zoom <= MIN_ZOOM ) {
166
- this . setState ( {
167
- zoom : MIN_ZOOM ,
168
- translationX : 0 ,
169
- translationY : 0 ,
170
- } ) ;
171
- return ;
172
- }
173
- this . setState ( {
174
- zoom : this . state . zoom - ZOOM_STEP ,
175
- } ) ;
176
- } ;
177
-
178
208
private onDownloadClick = ( ) => {
179
209
const a = document . createElement ( "a" ) ;
180
210
a . href = this . props . src ;
@@ -217,8 +247,8 @@ export default class ImageView extends React.Component<IProps, IState> {
217
247
if ( ev . button !== 0 ) return ;
218
248
219
249
// Zoom in if we are completely zoomed out
220
- if ( this . state . zoom === MIN_ZOOM ) {
221
- this . setState ( { zoom : MAX_ZOOM } ) ;
250
+ if ( this . state . zoom === this . state . minZoom ) {
251
+ this . setState ( { zoom : this . state . maxZoom } ) ;
222
252
return ;
223
253
}
224
254
@@ -251,7 +281,7 @@ export default class ImageView extends React.Component<IProps, IState> {
251
281
Math . abs ( this . state . translationY - this . previousY ) < ZOOM_DISTANCE
252
282
) {
253
283
this . setState ( {
254
- zoom : MIN_ZOOM ,
284
+ zoom : this . state . minZoom ,
255
285
translationX : 0 ,
256
286
translationY : 0 ,
257
287
} ) ;
@@ -286,17 +316,20 @@ export default class ImageView extends React.Component<IProps, IState> {
286
316
287
317
render ( ) {
288
318
const showEventMeta = ! ! this . props . mxEvent ;
319
+ const zoomingDisabled = this . state . maxZoom === this . state . minZoom ;
289
320
290
321
let cursor ;
291
322
if ( this . state . moving ) {
292
323
cursor = "grabbing" ;
293
- } else if ( this . state . zoom === MIN_ZOOM ) {
324
+ } else if ( zoomingDisabled ) {
325
+ cursor = "default" ;
326
+ } else if ( this . state . zoom === this . state . minZoom ) {
294
327
cursor = "zoom-in" ;
295
328
} else {
296
329
cursor = "zoom-out" ;
297
330
}
298
331
const rotationDegrees = this . state . rotation + "deg" ;
299
- const zoomPercentage = this . state . zoom / 100 ;
332
+ const zoom = this . state . zoom ;
300
333
const translatePixelsX = this . state . translationX + "px" ;
301
334
const translatePixelsY = this . state . translationY + "px" ;
302
335
// The order of the values is important!
@@ -308,7 +341,7 @@ export default class ImageView extends React.Component<IProps, IState> {
308
341
transition : this . state . moving ? null : "transform 200ms ease 0s" ,
309
342
transform : `translateX(${ translatePixelsX } )
310
343
translateY(${ translatePixelsY } )
311
- scale(${ zoomPercentage } )
344
+ scale(${ zoom } )
312
345
rotate(${ rotationDegrees } )` ,
313
346
} ;
314
347
@@ -380,6 +413,25 @@ export default class ImageView extends React.Component<IProps, IState> {
380
413
) ;
381
414
}
382
415
416
+ let zoomOutButton ;
417
+ let zoomInButton ;
418
+ if ( ! zoomingDisabled ) {
419
+ zoomOutButton = (
420
+ < AccessibleTooltipButton
421
+ className = "mx_ImageView_button mx_ImageView_button_zoomOut"
422
+ title = { _t ( "Zoom out" ) }
423
+ onClick = { this . onZoomOutClick } >
424
+ </ AccessibleTooltipButton >
425
+ ) ;
426
+ zoomInButton = (
427
+ < AccessibleTooltipButton
428
+ className = "mx_ImageView_button mx_ImageView_button_zoomIn"
429
+ title = { _t ( "Zoom in" ) }
430
+ onClick = { this . onZoomInClick } >
431
+ </ AccessibleTooltipButton >
432
+ ) ;
433
+ }
434
+
383
435
return (
384
436
< FocusLock
385
437
returnFocus = { true }
@@ -403,16 +455,8 @@ export default class ImageView extends React.Component<IProps, IState> {
403
455
title = { _t ( "Rotate Left" ) }
404
456
onClick = { this . onRotateCounterClockwiseClick } >
405
457
</ AccessibleTooltipButton >
406
- < AccessibleTooltipButton
407
- className = "mx_ImageView_button mx_ImageView_button_zoomOut"
408
- title = { _t ( "Zoom out" ) }
409
- onClick = { this . onZoomOutClick } >
410
- </ AccessibleTooltipButton >
411
- < AccessibleTooltipButton
412
- className = "mx_ImageView_button mx_ImageView_button_zoomIn"
413
- title = { _t ( "Zoom in" ) }
414
- onClick = { this . onZoomInClick } >
415
- </ AccessibleTooltipButton >
458
+ { zoomOutButton }
459
+ { zoomInButton }
416
460
< AccessibleTooltipButton
417
461
className = "mx_ImageView_button mx_ImageView_button_download"
418
462
title = { _t ( "Download" ) }
@@ -427,11 +471,14 @@ export default class ImageView extends React.Component<IProps, IState> {
427
471
{ this . renderContextMenu ( ) }
428
472
</ div >
429
473
</ div >
430
- < div className = "mx_ImageView_image_wrapper" >
474
+ < div
475
+ className = "mx_ImageView_image_wrapper"
476
+ ref = { this . imageWrapper } >
431
477
< img
432
478
src = { this . props . src }
433
479
title = { this . props . name }
434
480
style = { style }
481
+ ref = { this . image }
435
482
className = "mx_ImageView_image"
436
483
draggable = { true }
437
484
onMouseDown = { this . onStartMoving }
0 commit comments