mirror of
https://github.com/microsoft/vscode.git
synced 2026-05-02 06:21:50 +01:00
Fixes #71653 Adds two new commands, `imagePreview.zoomIn` and `imagePreview.zoomOut`. These zoom the active image preview in or out. They have no default keybinding but users can configure one for them
321 lines
6.7 KiB
JavaScript
321 lines
6.7 KiB
JavaScript
/*---------------------------------------------------------------------------------------------
|
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
|
* Licensed under the MIT License. See License.txt in the project root for license information.
|
|
*--------------------------------------------------------------------------------------------*/
|
|
// @ts-check
|
|
"use strict";
|
|
|
|
(function () {
|
|
/**
|
|
* @param {number} value
|
|
* @param {number} min
|
|
* @param {number} max
|
|
* @return {number}
|
|
*/
|
|
function clamp(value, min, max) {
|
|
return Math.min(Math.max(value, min), max);
|
|
}
|
|
|
|
function getSettings() {
|
|
const element = document.getElementById('image-preview-settings');
|
|
if (element) {
|
|
const data = element.getAttribute('data-settings');
|
|
if (data) {
|
|
return JSON.parse(data);
|
|
}
|
|
}
|
|
|
|
throw new Error(`Could not load settings`);
|
|
}
|
|
|
|
/**
|
|
* Enable image-rendering: pixelated for images scaled by more than this.
|
|
*/
|
|
const PIXELATION_THRESHOLD = 3;
|
|
|
|
const SCALE_PINCH_FACTOR = 0.075;
|
|
const MAX_SCALE = 20;
|
|
const MIN_SCALE = 0.1;
|
|
|
|
const zoomLevels = [
|
|
0.1,
|
|
0.2,
|
|
0.3,
|
|
0.4,
|
|
0.5,
|
|
0.6,
|
|
0.7,
|
|
0.8,
|
|
0.9,
|
|
1,
|
|
1.5,
|
|
2,
|
|
3,
|
|
5,
|
|
7,
|
|
10,
|
|
15,
|
|
20
|
|
];
|
|
|
|
const settings = getSettings();
|
|
const isMac = settings.isMac;
|
|
|
|
const vscode = acquireVsCodeApi();
|
|
|
|
const initialState = vscode.getState() || { scale: 'fit', offsetX: 0, offsetY: 0 };
|
|
|
|
// State
|
|
let scale = initialState.scale;
|
|
let ctrlPressed = false;
|
|
let altPressed = false;
|
|
let hasLoadedImage = false;
|
|
let consumeClick = false;
|
|
|
|
// Elements
|
|
const container = document.body;
|
|
const image = document.createElement('img');
|
|
|
|
function updateScale(newScale) {
|
|
if (!image || !hasLoadedImage || !image.parentElement) {
|
|
return;
|
|
}
|
|
|
|
if (newScale === 'fit') {
|
|
scale = 'fit';
|
|
image.classList.add('scale-to-fit');
|
|
image.classList.remove('pixelated');
|
|
image.style.minWidth = 'auto';
|
|
image.style.width = 'auto';
|
|
vscode.setState(undefined);
|
|
} else {
|
|
scale = clamp(newScale, MIN_SCALE, MAX_SCALE);
|
|
if (scale >= PIXELATION_THRESHOLD) {
|
|
image.classList.add('pixelated');
|
|
} else {
|
|
image.classList.remove('pixelated');
|
|
}
|
|
|
|
const dx = (window.scrollX + container.clientWidth / 2) / container.scrollWidth;
|
|
const dy = (window.scrollY + container.clientHeight / 2) / container.scrollHeight;
|
|
|
|
image.classList.remove('scale-to-fit');
|
|
image.style.minWidth = `${(image.naturalWidth * scale)}px`;
|
|
image.style.width = `${(image.naturalWidth * scale)}px`;
|
|
|
|
const newScrollX = container.scrollWidth * dx - container.clientWidth / 2;
|
|
const newScrollY = container.scrollHeight * dy - container.clientHeight / 2;
|
|
|
|
window.scrollTo(newScrollX, newScrollY);
|
|
|
|
vscode.setState({ scale: scale, offsetX: newScrollX, offsetY: newScrollY });
|
|
}
|
|
|
|
vscode.postMessage({
|
|
type: 'zoom',
|
|
value: scale
|
|
});
|
|
}
|
|
|
|
function changeActive(value) {
|
|
if (value) {
|
|
container.classList.add('zoom-in');
|
|
consumeClick = true;
|
|
} else {
|
|
ctrlPressed = false;
|
|
altPressed = false;
|
|
container.classList.remove('zoom-out');
|
|
container.classList.remove('zoom-in');
|
|
}
|
|
}
|
|
|
|
function firstZoom() {
|
|
if (!image || !hasLoadedImage) {
|
|
return;
|
|
}
|
|
|
|
scale = image.clientWidth / image.naturalWidth;
|
|
updateScale(scale);
|
|
}
|
|
|
|
function zoomIn() {
|
|
if (scale === 'fit') {
|
|
firstZoom();
|
|
}
|
|
|
|
let i = 0;
|
|
for (; i < zoomLevels.length; ++i) {
|
|
if (zoomLevels[i] > scale) {
|
|
break;
|
|
}
|
|
}
|
|
updateScale(zoomLevels[i] || MAX_SCALE);
|
|
}
|
|
|
|
function zoomOut() {
|
|
if (scale === 'fit') {
|
|
firstZoom();
|
|
}
|
|
|
|
let i = zoomLevels.length - 1;
|
|
for (; i >= 0; --i) {
|
|
if (zoomLevels[i] < scale) {
|
|
break;
|
|
}
|
|
}
|
|
updateScale(zoomLevels[i] || MIN_SCALE);
|
|
}
|
|
|
|
window.addEventListener('keydown', (/** @type {KeyboardEvent} */ e) => {
|
|
if (!image || !hasLoadedImage) {
|
|
return;
|
|
}
|
|
ctrlPressed = e.ctrlKey;
|
|
altPressed = e.altKey;
|
|
|
|
if (isMac ? altPressed : ctrlPressed) {
|
|
container.classList.remove('zoom-in');
|
|
container.classList.add('zoom-out');
|
|
}
|
|
});
|
|
|
|
window.addEventListener('keyup', (/** @type {KeyboardEvent} */ e) => {
|
|
if (!image || !hasLoadedImage) {
|
|
return;
|
|
}
|
|
|
|
ctrlPressed = e.ctrlKey;
|
|
altPressed = e.altKey;
|
|
|
|
if (!(isMac ? altPressed : ctrlPressed)) {
|
|
container.classList.remove('zoom-out');
|
|
container.classList.add('zoom-in');
|
|
}
|
|
});
|
|
|
|
container.addEventListener('mousedown', (/** @type {MouseEvent} */ e) => {
|
|
if (!image || !hasLoadedImage) {
|
|
return;
|
|
}
|
|
|
|
if (e.button !== 0) {
|
|
return;
|
|
}
|
|
|
|
consumeClick = false;
|
|
});
|
|
|
|
container.addEventListener('click', (/** @type {MouseEvent} */ e) => {
|
|
if (!image || !hasLoadedImage) {
|
|
return;
|
|
}
|
|
|
|
if (e.button !== 0) {
|
|
return;
|
|
}
|
|
|
|
ctrlPressed = e.ctrlKey;
|
|
altPressed = e.altKey;
|
|
|
|
if (isMac ? altPressed : ctrlPressed) {
|
|
container.classList.remove('zoom-in');
|
|
container.classList.add('zoom-out');
|
|
}
|
|
|
|
if (consumeClick) {
|
|
consumeClick = false;
|
|
return;
|
|
}
|
|
// left click
|
|
if (scale === 'fit') {
|
|
firstZoom();
|
|
}
|
|
|
|
if (!(isMac ? altPressed : ctrlPressed)) { // zoom in
|
|
zoomIn();
|
|
} else {
|
|
zoomOut();
|
|
}
|
|
});
|
|
|
|
container.addEventListener('wheel', (/** @type {WheelEvent} */ e) => {
|
|
if (!image || !hasLoadedImage) {
|
|
return;
|
|
}
|
|
|
|
const isScrollWheelKeyPressed = isMac ? altPressed : ctrlPressed;
|
|
if (!isScrollWheelKeyPressed && !e.ctrlKey) { // pinching is reported as scroll wheel + ctrl
|
|
return;
|
|
}
|
|
|
|
if (scale === 'fit') {
|
|
firstZoom();
|
|
}
|
|
|
|
let delta = e.deltaY > 0 ? 1 : -1;
|
|
updateScale(scale * (1 - delta * SCALE_PINCH_FACTOR));
|
|
});
|
|
|
|
window.addEventListener('scroll', () => {
|
|
if (!image || !hasLoadedImage || !image.parentElement || scale === 'fit') {
|
|
return;
|
|
}
|
|
|
|
const entry = vscode.getState();
|
|
if (entry) {
|
|
vscode.setState({ scale: entry.scale, offsetX: window.scrollX, offsetY: window.scrollY });
|
|
}
|
|
});
|
|
|
|
container.classList.add('image');
|
|
|
|
image.classList.add('scale-to-fit');
|
|
|
|
image.addEventListener('load', () => {
|
|
hasLoadedImage = true;
|
|
|
|
vscode.postMessage({
|
|
type: 'size',
|
|
value: `${image.naturalWidth}x${image.naturalHeight}`,
|
|
});
|
|
|
|
document.body.classList.remove('loading');
|
|
document.body.classList.add('ready');
|
|
document.body.append(image);
|
|
|
|
updateScale(scale);
|
|
|
|
if (initialState.scale !== 'fit') {
|
|
window.scrollTo(initialState.offsetX, initialState.offsetY);
|
|
}
|
|
});
|
|
|
|
image.addEventListener('error', () => {
|
|
hasLoadedImage = true;
|
|
document.body.classList.add('error');
|
|
document.body.classList.remove('loading');
|
|
});
|
|
|
|
image.src = decodeURI(settings.src);
|
|
|
|
window.addEventListener('message', e => {
|
|
switch (e.data.type) {
|
|
case 'setScale':
|
|
updateScale(e.data.scale);
|
|
break;
|
|
|
|
case 'setActive':
|
|
changeActive(e.data.value);
|
|
break;
|
|
|
|
case 'zoomIn':
|
|
zoomIn();
|
|
break;
|
|
|
|
case 'zoomOut':
|
|
zoomOut();
|
|
break;
|
|
}
|
|
});
|
|
}());
|