Skip to content
This repository was archived by the owner on Sep 11, 2024. It is now read-only.

Commit af14f83

Browse files
authored
Merge pull request #5927 from matrix-org/jryans/image-view-zoom-release
[Release] Dynamic max and min zoom in the new ImageView
2 parents 41f4293 + 9f1d17b commit af14f83

File tree

3 files changed

+114
-68
lines changed

3 files changed

+114
-68
lines changed

res/css/views/elements/_ImageView.scss

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,7 @@ limitations under the License.
3131

3232
.mx_ImageView_image {
3333
pointer-events: all;
34-
max-width: 95%;
35-
max-height: 95%;
34+
flex-shrink: 0;
3635
}
3736

3837
.mx_ImageView_panel {

src/components/views/elements/ImageView.tsx

Lines changed: 111 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -34,16 +34,15 @@ import {RoomPermalinkCreator} from "../../../utils/permalinks/Permalinks"
3434
import {MatrixEvent} from "matrix-js-sdk/src/models/event";
3535
import {normalizeWheelEvent} from "../../../utils/Mouse";
3636

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;
3939
// This is used for the buttons
40-
const ZOOM_STEP = 10;
40+
const ZOOM_STEP = 0.10;
4141
// This is used for mouse wheel events
42-
const ZOOM_COEFFICIENT = 0.5;
42+
const ZOOM_COEFFICIENT = 0.0025;
4343
// If we have moved only this much we can zoom
4444
const ZOOM_DISTANCE = 10;
4545

46-
4746
interface IProps {
4847
src: string, // the source of the image being displayed
4948
name?: string, // the main title ('name') for the image
@@ -62,8 +61,10 @@ interface IProps {
6261
}
6362

6463
interface IState {
65-
rotation: number,
6664
zoom: number,
65+
minZoom: number,
66+
maxZoom: number,
67+
rotation: number,
6768
translationX: number,
6869
translationY: number,
6970
moving: boolean,
@@ -75,8 +76,10 @@ export default class ImageView extends React.Component<IProps, IState> {
7576
constructor(props) {
7677
super(props);
7778
this.state = {
79+
zoom: 0,
80+
minZoom: MAX_SCALE,
81+
maxZoom: MAX_SCALE,
7882
rotation: 0,
79-
zoom: MIN_ZOOM,
8083
translationX: 0,
8184
translationY: 0,
8285
moving: false,
@@ -87,6 +90,8 @@ export default class ImageView extends React.Component<IProps, IState> {
8790
// XXX: Refs to functional components
8891
private contextMenuButton = createRef<any>();
8992
private focusLock = createRef<any>();
93+
private imageWrapper = createRef<HTMLDivElement>();
94+
private image = createRef<HTMLImageElement>();
9095

9196
private initX = 0;
9297
private initY = 0;
@@ -99,43 +104,93 @@ export default class ImageView extends React.Component<IProps, IState> {
99104
// We have to use addEventListener() because the listener
100105
// needs to be passive in order to work with Chromium
101106
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();
102113
}
103114

104115
componentWillUnmount() {
105116
this.focusLock.current.removeEventListener('wheel', this.onWheel);
106117
}
107118

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;
113138
}
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;
115143

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+
}
119150

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;
122153

123-
if (newZoom <= MIN_ZOOM) {
154+
if (newZoom <= this.state.minZoom) {
124155
this.setState({
125-
zoom: MIN_ZOOM,
156+
zoom: this.state.minZoom,
126157
translationX: 0,
127158
translationY: 0,
128159
});
129160
return;
130161
}
131-
if (newZoom >= MAX_ZOOM) {
132-
this.setState({zoom: MAX_ZOOM});
162+
if (newZoom >= this.state.maxZoom) {
163+
this.setState({zoom: this.state.maxZoom});
133164
return;
134165
}
135166

136167
this.setState({
137168
zoom: newZoom,
138169
});
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+
}
139194
};
140195

141196
private onRotateCounterClockwiseClick = () => {
@@ -150,31 +205,6 @@ export default class ImageView extends React.Component<IProps, IState> {
150205
this.setState({ rotation: rotationDegrees });
151206
};
152207

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-
178208
private onDownloadClick = () => {
179209
const a = document.createElement("a");
180210
a.href = this.props.src;
@@ -217,8 +247,8 @@ export default class ImageView extends React.Component<IProps, IState> {
217247
if (ev.button !== 0) return;
218248

219249
// 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});
222252
return;
223253
}
224254

@@ -251,7 +281,7 @@ export default class ImageView extends React.Component<IProps, IState> {
251281
Math.abs(this.state.translationY - this.previousY) < ZOOM_DISTANCE
252282
) {
253283
this.setState({
254-
zoom: MIN_ZOOM,
284+
zoom: this.state.minZoom,
255285
translationX: 0,
256286
translationY: 0,
257287
});
@@ -286,17 +316,20 @@ export default class ImageView extends React.Component<IProps, IState> {
286316

287317
render() {
288318
const showEventMeta = !!this.props.mxEvent;
319+
const zoomingDisabled = this.state.maxZoom === this.state.minZoom;
289320

290321
let cursor;
291322
if (this.state.moving) {
292323
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) {
294327
cursor = "zoom-in";
295328
} else {
296329
cursor = "zoom-out";
297330
}
298331
const rotationDegrees = this.state.rotation + "deg";
299-
const zoomPercentage = this.state.zoom/100;
332+
const zoom = this.state.zoom;
300333
const translatePixelsX = this.state.translationX + "px";
301334
const translatePixelsY = this.state.translationY + "px";
302335
// The order of the values is important!
@@ -308,7 +341,7 @@ export default class ImageView extends React.Component<IProps, IState> {
308341
transition: this.state.moving ? null : "transform 200ms ease 0s",
309342
transform: `translateX(${translatePixelsX})
310343
translateY(${translatePixelsY})
311-
scale(${zoomPercentage})
344+
scale(${zoom})
312345
rotate(${rotationDegrees})`,
313346
};
314347

@@ -380,6 +413,25 @@ export default class ImageView extends React.Component<IProps, IState> {
380413
);
381414
}
382415

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+
383435
return (
384436
<FocusLock
385437
returnFocus={true}
@@ -403,16 +455,8 @@ export default class ImageView extends React.Component<IProps, IState> {
403455
title={_t("Rotate Left")}
404456
onClick={ this.onRotateCounterClockwiseClick }>
405457
</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}
416460
<AccessibleTooltipButton
417461
className="mx_ImageView_button mx_ImageView_button_download"
418462
title={_t("Download")}
@@ -427,11 +471,14 @@ export default class ImageView extends React.Component<IProps, IState> {
427471
{this.renderContextMenu()}
428472
</div>
429473
</div>
430-
<div className="mx_ImageView_image_wrapper">
474+
<div
475+
className="mx_ImageView_image_wrapper"
476+
ref={this.imageWrapper}>
431477
<img
432478
src={this.props.src}
433479
title={this.props.name}
434480
style={style}
481+
ref={this.image}
435482
className="mx_ImageView_image"
436483
draggable={true}
437484
onMouseDown={this.onStartMoving}

src/i18n/strings/en_EN.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1917,10 +1917,10 @@
19171917
"collapse": "collapse",
19181918
"expand": "expand",
19191919
"%(count)s people you know have already joined|other": "%(count)s people you know have already joined",
1920-
"Rotate Right": "Rotate Right",
1921-
"Rotate Left": "Rotate Left",
19221920
"Zoom out": "Zoom out",
19231921
"Zoom in": "Zoom in",
1922+
"Rotate Right": "Rotate Right",
1923+
"Rotate Left": "Rotate Left",
19241924
"Download": "Download",
19251925
"Information": "Information",
19261926
"View message": "View message",

0 commit comments

Comments
 (0)