提交 9081507e 编写于 作者: B Brian Schlenker 提交者: Matt Bierner

Add ability to zoom in/out on all images (#38538)

* Add ability to zoom in on small images

* Update image viewer to allow pinch or click to zoom

Images are now always centered in the window. They initially start at
their native size, unless they would be larger than the window, in which
case they are contained within the window. Clicking increases the
zoom, and alt+click decreases it. Pinch to zoom and ctrl+scroll are also
supported.

* Update resourceViewer to improve image viewing experience

ResourceViewer now holds a cache of image scales so they stay the same
while flipping between editor tabs. Right clicking now returns the image
to its original scale. Pixelation only triggers for images 64x64 or
smaller, and only after the first zoom. Editor risizing is handled
thorugh the layout call to the binary editor, passed down to the
resource viewer.
上级 fc691f71
......@@ -10,11 +10,12 @@ import nls = require('vs/nls');
import mimes = require('vs/base/common/mime');
import URI from 'vs/base/common/uri';
import paths = require('vs/base/common/paths');
import { Builder, $ } from 'vs/base/browser/builder';
import { Builder, $, Dimension } from 'vs/base/browser/builder';
import DOM = require('vs/base/browser/dom');
import { DomScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableElement';
import { LRUCache } from 'vs/base/common/map';
import { Schemas } from 'vs/base/common/network';
import { clamp } from 'vs/base/common/numbers';
interface MapExtToMediaMimes {
[index: string]: string;
......@@ -78,6 +79,10 @@ export interface IResourceDescriptor {
mime: string;
}
enum ScaleDirection {
IN, OUT,
}
// Chrome is caching images very aggressively and so we use the ETag information to find out if
// we need to bypass the cache or not. We could always bypass the cache everytime we show the image
// however that has very bad impact on memory consumption because each time the image gets shown,
......@@ -104,6 +109,13 @@ function imageSrc(descriptor: IResourceDescriptor): string {
return cached.src;
}
// store the scale of an image so it can be restored when changing editor tabs
const IMAGE_SCALE_CACHE = new LRUCache<string, number>(100);
export interface ResourceViewerContext {
layout(dimension: Dimension);
}
/**
* Helper to actually render the given resource into the provided container. Will adjust scrollbar (if provided) automatically based on loading
* progress of the binary resource.
......@@ -117,13 +129,19 @@ export class ResourceViewer {
private static readonly MAX_IMAGE_SIZE = ResourceViewer.MB; // showing images inline is memory intense, so we have a limit
private static SCALE_PINCH_FACTOR = 0.1;
private static SCALE_FACTOR = 1.5;
private static MAX_SCALE = 20;
private static MIN_SCALE = 0.1;
private static PIXELATION_THRESHOLD = 64; // enable image-rendering: pixelated for images less than this
public static show(
descriptor: IResourceDescriptor,
container: Builder,
scrollbar: DomScrollableElement,
openExternal: (uri: URI) => void,
metadataClb?: (meta: string) => void
): void {
): ResourceViewerContext {
// Ensure CSS class
$(container).setClass('monaco-resource-viewer');
......@@ -144,28 +162,115 @@ export class ResourceViewer {
// Show Image inline unless they are large
if (mime.indexOf('image/') >= 0) {
if (ResourceViewer.inlineImage(descriptor)) {
const context = {
layout(dimension: Dimension) { }
};
$(container)
.empty()
.addClass('image')
.addClass('image', 'zoom-in')
.img({ src: imageSrc(descriptor) })
.addClass('untouched')
.on(DOM.EventType.LOAD, (e, img) => {
const imgElement = <HTMLImageElement>img.getHTMLElement();
if (imgElement.naturalWidth > imgElement.width || imgElement.naturalHeight > imgElement.height) {
$(container).addClass('oversized');
const cacheKey = descriptor.resource.toString();
let scaleDirection = ScaleDirection.IN;
let scale = IMAGE_SCALE_CACHE.get(cacheKey) || null;
if (scale) {
img.removeClass('untouched');
updateScale(scale);
}
img.on(DOM.EventType.CLICK, (e, img) => {
$(container).toggleClass('full-size');
function setImageWidth(width) {
img.style('width', `${width}px`);
img.style('height', 'auto');
}
scrollbar.scanDomNode();
});
function updateScale(newScale) {
scale = clamp(newScale, ResourceViewer.MIN_SCALE, ResourceViewer.MAX_SCALE);
setImageWidth(Math.floor(imgElement.naturalWidth * scale));
IMAGE_SCALE_CACHE.set(cacheKey, scale);
scrollbar.scanDomNode();
updateMetadata();
}
function updateMetadata() {
if (metadataClb) {
const scale = Math.round((imgElement.width / imgElement.naturalWidth) * 10000) / 100;
metadataClb(nls.localize('imgMeta', '{0}% {1}x{2} {3}',
scale,
imgElement.naturalWidth,
imgElement.naturalHeight,
ResourceViewer.formatSize(descriptor.size)));
}
}
if (metadataClb) {
metadataClb(nls.localize('imgMeta', "{0}x{1} {2}", imgElement.naturalWidth, imgElement.naturalHeight, ResourceViewer.formatSize(descriptor.size)));
context.layout = updateMetadata;
function firstZoom() {
const { clientWidth, naturalWidth } = imgElement;
setImageWidth(clientWidth);
img.removeClass('untouched');
if (imgElement.naturalWidth < ResourceViewer.PIXELATION_THRESHOLD
|| imgElement.naturalHeight < ResourceViewer.PIXELATION_THRESHOLD) {
img.addClass('pixelated');
}
scale = clientWidth / naturalWidth;
}
$(container)
.on(DOM.EventType.KEY_DOWN, (e: KeyboardEvent, c) => {
if (e.altKey) {
scaleDirection = ScaleDirection.OUT;
c.removeClass('zoom-in').addClass('zoom-out');
}
})
.on(DOM.EventType.KEY_UP, (e: KeyboardEvent, c) => {
if (!e.altKey) {
scaleDirection = ScaleDirection.IN;
c.removeClass('zoom-out').addClass('zoom-in');
}
});
$(container).on(DOM.EventType.MOUSE_DOWN, (e: MouseEvent) => {
if (scale === null) {
firstZoom();
}
// right click
if (e.button === 2) {
updateScale(1);
} else {
const scaleFactor = scaleDirection === ScaleDirection.IN
? ResourceViewer.SCALE_FACTOR
: 1 / ResourceViewer.SCALE_FACTOR;
updateScale(scale * scaleFactor);
}
});
$(container).on(DOM.EventType.WHEEL, (e: WheelEvent) => {
// pinching is reported as scroll wheel + ctrl
if (!e.ctrlKey) {
return;
}
if (scale === null) {
firstZoom();
}
// scrolling up, pinching out should increase the scale
const delta = -e.deltaY;
updateScale(scale + delta * ResourceViewer.SCALE_PINCH_FACTOR);
});
updateMetadata();
scrollbar.scanDomNode();
});
return context;
} else {
const imageContainer = $(container)
.empty()
......@@ -199,6 +304,8 @@ export class ResourceViewer {
scrollbar.scanDomNode();
}
return null;
}
private static inlineImage(descriptor: IResourceDescriptor): boolean {
......
......@@ -16,6 +16,7 @@
padding: 10px 10px 0 10px;
background-position: 0 0, 8px 8px;
background-size: 16px 16px;
display: grid;
}
.monaco-resource-viewer.image.full-size {
......@@ -34,18 +35,24 @@
linear-gradient(45deg, rgb(20, 20, 20) 25%, transparent 25%, transparent 75%, rgb(20, 20, 20) 75%, rgb(20, 20, 20));
}
.monaco-resource-viewer img {
.monaco-resource-viewer img.untouched {
max-width: 100%;
max-height: calc(100% - 10px); /* somehow this prevents scrollbars from showing up */
object-fit: contain;
}
.monaco-resource-viewer img.pixelated {
image-rendering: pixelated;
}
.monaco-resource-viewer img {
margin: auto; /* centers the image */
}
.monaco-resource-viewer.oversized img {
.monaco-resource-viewer.zoom-in {
cursor: zoom-in;
}
.monaco-resource-viewer.full-size img {
max-width: initial;
max-height: initial;
.monaco-resource-viewer.zoom-out {
cursor: zoom-out;
}
......
......@@ -10,7 +10,7 @@ import Event, { Emitter } from 'vs/base/common/event';
import URI from 'vs/base/common/uri';
import { TPromise } from 'vs/base/common/winjs.base';
import { Dimension, Builder, $ } from 'vs/base/browser/builder';
import { ResourceViewer } from 'vs/base/browser/ui/resourceviewer/resourceViewer';
import { ResourceViewer, ResourceViewerContext } from 'vs/base/browser/ui/resourceviewer/resourceViewer';
import { EditorModel, EditorInput, EditorOptions } from 'vs/workbench/common/editor';
import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor';
import { BinaryEditorModel } from 'vs/workbench/common/editor/binaryEditorModel';
......@@ -29,6 +29,7 @@ export abstract class BaseBinaryResourceEditor extends BaseEditor {
private binaryContainer: Builder;
private scrollbar: DomScrollableElement;
private resourceViewerContext: ResourceViewerContext;
constructor(
id: string,
......@@ -87,7 +88,7 @@ export abstract class BaseBinaryResourceEditor extends BaseEditor {
// Render Input
const model = <BinaryEditorModel>resolvedModel;
ResourceViewer.show(
this.resourceViewerContext = ResourceViewer.show(
{ name: model.getName(), resource: model.getResource(), size: model.getSize(), etag: model.getETag(), mime: model.getMime() },
this.binaryContainer,
this.scrollbar,
......@@ -132,6 +133,9 @@ export abstract class BaseBinaryResourceEditor extends BaseEditor {
// Pass on to Binary Container
this.binaryContainer.size(dimension.width, dimension.height);
this.scrollbar.scanDomNode();
if (this.resourceViewerContext) {
this.resourceViewerContext.layout(dimension);
}
}
public focus(): void {
......@@ -146,4 +150,4 @@ export abstract class BaseBinaryResourceEditor extends BaseEditor {
super.dispose();
}
}
\ No newline at end of file
}
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册