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

Dynamic max and min zoom in the new ImageView #5916

Merged
merged 20 commits into from
Apr 26, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions res/css/views/elements/_ImageView.scss
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,7 @@ limitations under the License.

.mx_ImageView_image {
pointer-events: all;
max-width: 95%;
max-height: 95%;
flex-shrink: 0;
}

.mx_ImageView_panel {
Expand Down
175 changes: 111 additions & 64 deletions src/components/views/elements/ImageView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,16 +34,15 @@ import {RoomPermalinkCreator} from "../../../utils/permalinks/Permalinks"
import {MatrixEvent} from "matrix-js-sdk/src/models/event";
import {normalizeWheelEvent} from "../../../utils/Mouse";

const MIN_ZOOM = 100;
const MAX_ZOOM = 300;
// Max scale to keep gaps around the image
const MAX_SCALE = 0.95;
// This is used for the buttons
const ZOOM_STEP = 10;
const ZOOM_STEP = 0.10;
// This is used for mouse wheel events
const ZOOM_COEFFICIENT = 0.5;
const ZOOM_COEFFICIENT = 0.0025;
// If we have moved only this much we can zoom
const ZOOM_DISTANCE = 10;


interface IProps {
src: string, // the source of the image being displayed
name?: string, // the main title ('name') for the image
Expand All @@ -62,8 +61,10 @@ interface IProps {
}

interface IState {
rotation: number,
zoom: number,
minZoom: number,
maxZoom: number,
rotation: number,
translationX: number,
translationY: number,
moving: boolean,
Expand All @@ -75,8 +76,10 @@ export default class ImageView extends React.Component<IProps, IState> {
constructor(props) {
super(props);
this.state = {
zoom: 0,
minZoom: MAX_SCALE,
maxZoom: MAX_SCALE,
rotation: 0,
zoom: MIN_ZOOM,
translationX: 0,
translationY: 0,
moving: false,
Expand All @@ -87,6 +90,8 @@ export default class ImageView extends React.Component<IProps, IState> {
// XXX: Refs to functional components
private contextMenuButton = createRef<any>();
private focusLock = createRef<any>();
private imageWrapper = createRef<HTMLDivElement>();
private image = createRef<HTMLImageElement>();

private initX = 0;
private initY = 0;
Expand All @@ -99,43 +104,93 @@ export default class ImageView extends React.Component<IProps, IState> {
// We have to use addEventListener() because the listener
// needs to be passive in order to work with Chromium
this.focusLock.current.addEventListener('wheel', this.onWheel, { passive: false });
// We want to recalculate zoom whenever the window's size changes
window.addEventListener("resize", this.calculateZoom);
// After the image loads for the first time we want to calculate the zoom
this.image.current.addEventListener("load", this.calculateZoom);
// Try to precalculate the zoom from width and height props
this.calculateZoom();
}

componentWillUnmount() {
this.focusLock.current.removeEventListener('wheel', this.onWheel);
}

private onKeyDown = (ev: KeyboardEvent) => {
if (ev.key === Key.ESCAPE) {
ev.stopPropagation();
ev.preventDefault();
this.props.onFinished();
private calculateZoom = () => {
const image = this.image.current;
const imageWrapper = this.imageWrapper.current;

const width = this.props.width || image.naturalWidth;
const height = this.props.height || image.naturalHeight;

const zoomX = imageWrapper.clientWidth / width;
const zoomY = imageWrapper.clientHeight / height;

// If the image is smaller in both dimensions set its the zoom to 1 to
// display it in its original size
if (zoomX >= 1 && zoomY >= 1) {
this.setState({
zoom: 1,
minZoom: 1,
maxZoom: 1,
});
return;
}
};
// We set minZoom to the min of the zoomX and zoomY to avoid overflow in
// any direction. We also multiply by MAX_SCALE to get a gap around the
// image by default
const minZoom = Math.min(zoomX, zoomY) * MAX_SCALE;

private onWheel = (ev: WheelEvent) => {
ev.stopPropagation();
ev.preventDefault();
if (this.state.zoom <= this.state.minZoom) this.setState({zoom: minZoom});
this.setState({
minZoom: minZoom,
maxZoom: 1,
});
}

const {deltaY} = normalizeWheelEvent(ev);
const newZoom = this.state.zoom - (deltaY * ZOOM_COEFFICIENT);
private zoom(delta: number) {
const newZoom = this.state.zoom + delta;

if (newZoom <= MIN_ZOOM) {
if (newZoom <= this.state.minZoom) {
this.setState({
zoom: MIN_ZOOM,
zoom: this.state.minZoom,
translationX: 0,
translationY: 0,
});
return;
}
if (newZoom >= MAX_ZOOM) {
this.setState({zoom: MAX_ZOOM});
if (newZoom >= this.state.maxZoom) {
this.setState({zoom: this.state.maxZoom});
return;
}

this.setState({
zoom: newZoom,
});
}

private onWheel = (ev: WheelEvent) => {
ev.stopPropagation();
ev.preventDefault();

const {deltaY} = normalizeWheelEvent(ev);
this.zoom(-(deltaY * ZOOM_COEFFICIENT));
};

private onZoomInClick = () => {
SimonBrandner marked this conversation as resolved.
Show resolved Hide resolved
this.zoom(ZOOM_STEP);
};

private onZoomOutClick = () => {
this.zoom(-ZOOM_STEP);
};

private onKeyDown = (ev: KeyboardEvent) => {
if (ev.key === Key.ESCAPE) {
ev.stopPropagation();
ev.preventDefault();
this.props.onFinished();
}
};

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

private onZoomInClick = () => {
if (this.state.zoom >= MAX_ZOOM) {
this.setState({zoom: MAX_ZOOM});
return;
}

this.setState({
zoom: this.state.zoom + ZOOM_STEP,
});
};

private onZoomOutClick = () => {
if (this.state.zoom <= MIN_ZOOM) {
this.setState({
zoom: MIN_ZOOM,
translationX: 0,
translationY: 0,
});
return;
}
this.setState({
zoom: this.state.zoom - ZOOM_STEP,
});
};

private onDownloadClick = () => {
const a = document.createElement("a");
a.href = this.props.src;
Expand Down Expand Up @@ -217,8 +247,8 @@ export default class ImageView extends React.Component<IProps, IState> {
if (ev.button !== 0) return;

// Zoom in if we are completely zoomed out
if (this.state.zoom === MIN_ZOOM) {
this.setState({zoom: MAX_ZOOM});
if (this.state.zoom === this.state.minZoom) {
this.setState({zoom: this.state.maxZoom});
return;
}

Expand Down Expand Up @@ -251,7 +281,7 @@ export default class ImageView extends React.Component<IProps, IState> {
Math.abs(this.state.translationY - this.previousY) < ZOOM_DISTANCE
) {
this.setState({
zoom: MIN_ZOOM,
zoom: this.state.minZoom,
translationX: 0,
translationY: 0,
});
Expand Down Expand Up @@ -286,17 +316,20 @@ export default class ImageView extends React.Component<IProps, IState> {

render() {
const showEventMeta = !!this.props.mxEvent;
const zoomingDisabled = this.state.maxZoom === this.state.minZoom;

let cursor;
if (this.state.moving) {
cursor= "grabbing";
} else if (this.state.zoom === MIN_ZOOM) {
} else if (zoomingDisabled) {
cursor = "default";
} else if (this.state.zoom === this.state.minZoom) {
cursor = "zoom-in";
} else {
cursor = "zoom-out";
}
const rotationDegrees = this.state.rotation + "deg";
const zoomPercentage = this.state.zoom/100;
const zoom = this.state.zoom;
const translatePixelsX = this.state.translationX + "px";
const translatePixelsY = this.state.translationY + "px";
// The order of the values is important!
Expand All @@ -308,7 +341,7 @@ export default class ImageView extends React.Component<IProps, IState> {
transition: this.state.moving ? null : "transform 200ms ease 0s",
transform: `translateX(${translatePixelsX})
translateY(${translatePixelsY})
scale(${zoomPercentage})
scale(${zoom})
rotate(${rotationDegrees})`,
};

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

let zoomOutButton;
let zoomInButton;
if (!zoomingDisabled) {
zoomOutButton = (
<AccessibleTooltipButton
className="mx_ImageView_button mx_ImageView_button_zoomOut"
title={_t("Zoom out")}
onClick={this.onZoomOutClick}>
</AccessibleTooltipButton>
);
zoomInButton = (
<AccessibleTooltipButton
className="mx_ImageView_button mx_ImageView_button_zoomIn"
title={_t("Zoom in")}
onClick={ this.onZoomInClick }>
</AccessibleTooltipButton>
);
}

return (
<FocusLock
returnFocus={true}
Expand All @@ -403,16 +455,8 @@ export default class ImageView extends React.Component<IProps, IState> {
title={_t("Rotate Left")}
onClick={ this.onRotateCounterClockwiseClick }>
</AccessibleTooltipButton>
<AccessibleTooltipButton
className="mx_ImageView_button mx_ImageView_button_zoomOut"
title={_t("Zoom out")}
onClick={ this.onZoomOutClick }>
</AccessibleTooltipButton>
<AccessibleTooltipButton
className="mx_ImageView_button mx_ImageView_button_zoomIn"
title={_t("Zoom in")}
onClick={ this.onZoomInClick }>
</AccessibleTooltipButton>
{zoomOutButton}
{zoomInButton}
<AccessibleTooltipButton
className="mx_ImageView_button mx_ImageView_button_download"
title={_t("Download")}
Expand All @@ -427,11 +471,14 @@ export default class ImageView extends React.Component<IProps, IState> {
{this.renderContextMenu()}
</div>
</div>
<div className="mx_ImageView_image_wrapper">
<div
className="mx_ImageView_image_wrapper"
ref={this.imageWrapper}>
<img
src={this.props.src}
title={this.props.name}
style={style}
ref={this.image}
className="mx_ImageView_image"
draggable={true}
onMouseDown={this.onStartMoving}
Expand Down
4 changes: 2 additions & 2 deletions src/i18n/strings/en_EN.json
Original file line number Diff line number Diff line change
Expand Up @@ -1923,10 +1923,10 @@
"%(count)s members including %(commaSeparatedMembers)s|one": "%(commaSeparatedMembers)s",
"%(count)s people you know have already joined|other": "%(count)s people you know have already joined",
"%(count)s people you know have already joined|one": "%(count)s person you know has already joined",
"Rotate Right": "Rotate Right",
"Rotate Left": "Rotate Left",
"Zoom out": "Zoom out",
"Zoom in": "Zoom in",
"Rotate Right": "Rotate Right",
"Rotate Left": "Rotate Left",
"Download": "Download",
"Information": "Information",
"View message": "View message",
Expand Down