in src/vs/workbench/browser/parts/editor/resourceViewer.ts [349:568]
static create(
container: HTMLElement,
descriptor: IResourceDescriptor,
fileService: IFileService,
scrollbar: DomScrollableElement,
delegate: ResourceViewerDelegate,
@IInstantiationService instantiationService: IInstantiationService,
) {
const disposables = new DisposableStore();
const zoomStatusbarItem = disposables.add(instantiationService.createInstance(ZoomStatusbarItem,
newScale => updateScale(newScale)));
const context: ResourceViewerContext = {
layout(dimension: DOM.Dimension) { },
dispose: () => disposables.dispose()
};
const cacheKey = `${descriptor.resource.toString()}:${descriptor.etag}`;
let ctrlPressed = false;
let altPressed = false;
const initialState: ImageState = InlineImageView.imageStateCache.get(cacheKey) || { scale: 'fit', offsetX: 0, offsetY: 0 };
let scale = initialState.scale;
let image: HTMLImageElement | null = null;
function updateScale(newScale: Scale) {
if (!image || !image.parentElement) {
return;
}
if (newScale === 'fit') {
scale = 'fit';
DOM.addClass(image, 'scale-to-fit');
DOM.removeClass(image, 'pixelated');
image.style.minWidth = 'auto';
image.style.width = 'auto';
InlineImageView.imageStateCache.delete(cacheKey);
} else {
const oldWidth = image.width;
const oldHeight = image.height;
scale = clamp(newScale, InlineImageView.MIN_SCALE, InlineImageView.MAX_SCALE);
if (scale >= InlineImageView.PIXELATION_THRESHOLD) {
DOM.addClass(image, 'pixelated');
} else {
DOM.removeClass(image, 'pixelated');
}
const { scrollTop, scrollLeft } = image.parentElement;
const dx = (scrollLeft + image.parentElement.clientWidth / 2) / image.parentElement.scrollWidth;
const dy = (scrollTop + image.parentElement.clientHeight / 2) / image.parentElement.scrollHeight;
DOM.removeClass(image, 'scale-to-fit');
image.style.minWidth = `${(image.naturalWidth * scale)}px`;
image.style.width = `${(image.naturalWidth * scale)}px`;
const newWidth = image.width;
const scaleFactor = (newWidth - oldWidth) / oldWidth;
const newScrollLeft = ((oldWidth * scaleFactor * dx) + scrollLeft);
const newScrollTop = ((oldHeight * scaleFactor * dy) + scrollTop);
scrollbar.setScrollPosition({
scrollLeft: newScrollLeft,
scrollTop: newScrollTop,
});
InlineImageView.imageStateCache.set(cacheKey, { scale: scale, offsetX: newScrollLeft, offsetY: newScrollTop });
}
zoomStatusbarItem.updateStatusbar(scale);
scrollbar.scanDomNode();
}
function firstZoom() {
if (!image) {
return;
}
scale = image.clientWidth / image.naturalWidth;
updateScale(scale);
}
disposables.add(DOM.addDisposableListener(window, DOM.EventType.KEY_DOWN, (e: KeyboardEvent) => {
if (!image) {
return;
}
ctrlPressed = e.ctrlKey;
altPressed = e.altKey;
if (platform.isMacintosh ? altPressed : ctrlPressed) {
DOM.removeClass(container, 'zoom-in');
DOM.addClass(container, 'zoom-out');
}
}));
disposables.add(DOM.addDisposableListener(window, DOM.EventType.KEY_UP, (e: KeyboardEvent) => {
if (!image) {
return;
}
ctrlPressed = e.ctrlKey;
altPressed = e.altKey;
if (!(platform.isMacintosh ? altPressed : ctrlPressed)) {
DOM.removeClass(container, 'zoom-out');
DOM.addClass(container, 'zoom-in');
}
}));
disposables.add(DOM.addDisposableListener(container, DOM.EventType.CLICK, (e: MouseEvent) => {
if (!image) {
return;
}
if (e.button !== 0) {
return;
}
// left click
if (scale === 'fit') {
firstZoom();
}
if (!(platform.isMacintosh ? altPressed : ctrlPressed)) { // zoom in
let i = 0;
for (; i < InlineImageView.zoomLevels.length; ++i) {
if (InlineImageView.zoomLevels[i] > scale) {
break;
}
}
updateScale(InlineImageView.zoomLevels[i] || InlineImageView.MAX_SCALE);
} else {
let i = InlineImageView.zoomLevels.length - 1;
for (; i >= 0; --i) {
if (InlineImageView.zoomLevels[i] < scale) {
break;
}
}
updateScale(InlineImageView.zoomLevels[i] || InlineImageView.MIN_SCALE);
}
}));
disposables.add(DOM.addDisposableListener(container, DOM.EventType.WHEEL, (e: WheelEvent) => {
if (!image) {
return;
}
const isScrollWheelKeyPressed = platform.isMacintosh ? altPressed : ctrlPressed;
if (!isScrollWheelKeyPressed && !e.ctrlKey) { // pinching is reported as scroll wheel + ctrl
return;
}
e.preventDefault();
e.stopPropagation();
if (scale === 'fit') {
firstZoom();
}
let delta = e.deltaY > 0 ? 1 : -1;
updateScale(scale as number * (1 - delta * InlineImageView.SCALE_PINCH_FACTOR));
}));
disposables.add(DOM.addDisposableListener(container, DOM.EventType.SCROLL, () => {
if (!image || !image.parentElement || scale === 'fit') {
return;
}
const entry = InlineImageView.imageStateCache.get(cacheKey);
if (entry) {
const { scrollTop, scrollLeft } = image.parentElement;
InlineImageView.imageStateCache.set(cacheKey, { scale: entry.scale, offsetX: scrollLeft, offsetY: scrollTop });
}
}));
DOM.clearNode(container);
DOM.addClasses(container, 'image', 'zoom-in');
image = DOM.append(container, DOM.$<HTMLImageElement>('img.scale-to-fit'));
image.style.visibility = 'hidden';
disposables.add(DOM.addDisposableListener(image, DOM.EventType.LOAD, e => {
if (!image) {
return;
}
if (typeof descriptor.size === 'number') {
delegate.metadataClb(nls.localize('imgMeta', '{0}x{1} {2}', image.naturalWidth, image.naturalHeight, BinarySize.formatSize(descriptor.size)));
} else {
delegate.metadataClb(nls.localize('imgMetaNoSize', '{0}x{1}', image.naturalWidth, image.naturalHeight));
}
scrollbar.scanDomNode();
image.style.visibility = 'visible';
updateScale(scale);
if (initialState.scale !== 'fit') {
scrollbar.setScrollPosition({
scrollLeft: initialState.offsetX,
scrollTop: initialState.offsetY,
});
}
}));
InlineImageView.imageSrc(descriptor, fileService).then(src => {
const img = container.querySelector('img');
if (img) {
if (typeof src === 'string') {
img.src = src;
} else {
const url = URL.createObjectURL(src);
disposables.add(toDisposable(() => URL.revokeObjectURL(url)));
img.src = url;
}
}
});
return context;
}