Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[image-view] Add scroll support to zoom-in and zoom-out #1197

Open
wants to merge 4 commits into
base: updated-latest-electron
Choose a base branch
from
Open
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
164 changes: 85 additions & 79 deletions packages/image-view/lib/image-editor-view.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ const {Emitter, CompositeDisposable, Disposable} = require('atom')
const etch = require('etch')
const $ = etch.dom

// View that renders the image of an {ImageEditor}.
module.exports =
class ImageEditorView {
constructor (editor) {
Expand All @@ -12,27 +11,27 @@ class ImageEditorView {
this.disposables = new CompositeDisposable()
this.imageSize = fs.statSync(this.editor.getPath()).size
this.loaded = false
this.mode = 'zoom-to-fit'
this.percentageStep = 4
this.steps = [0.1, 0.25, 0.5, 0.75, 1.0, 1.25, 1.5, 2, 3, 4, 5, 7.5, 10]
this.zoomFactor = 1.00
etch.initialize(this)

this.refs.image.style.display = 'none'
this.updateImageURI()

this.disposables.add(this.editor.onDidChange(() => this.updateImageURI()))
this.disposables.add(atom.commands.add(this.element, {
'image-view:reload': () => this.updateImageURI(),
'image-view:zoom-in': () => this.zoomIn(),
'image-view:zoom-out': () => this.zoomOut(),
'image-view:zoom-to-fit': () => this.zoomToFit(),
'image-view:reset-zoom': () => this.resetZoom(),
'core:move-up': () => { this.scrollUp() },
'core:move-down': () => { this.scrollDown() },
'core:page-up': () => { this.pageUp() },
'core:page-down': () => { this.pageDown() },
'core:move-to-top': () => { this.scrollToTop() },
'core:move-to-bottom': () => { this.scrollToBottom() }
'image-view:zoom-to-fit': () => this.zoomToFit(),
'core:move-up': () => this.scrollUp(),
'core:move-down': () => this.scrollDown(),
'core:move-left': () => this.scrollLeft(),
'core:move-right': () => this.scrollRight(),
'core:page-up': () => this.pageUp(),
'core:page-down': () => this.pageDown(),
'core:move-to-top': () => this.scrollToTop(),
'core:move-to-bottom': () => this.scrollToBottom()
}))

this.refs.image.onload = () => {
Expand All @@ -43,6 +42,7 @@ class ImageEditorView {
this.refs.image.style.display = ''
this.defaultBackgroundColor = atom.config.get('image-view.defaultBackgroundColor')
this.refs.imageContainer.setAttribute('background', this.defaultBackgroundColor)
this.zoomToFit(1)
this.emitter.emit('did-load')
}

Expand Down Expand Up @@ -86,21 +86,36 @@ class ImageEditorView {
}
this.refs.zoomToFitButton.addEventListener('click', zoomToFitClickHandler)
this.disposables.add(new Disposable(() => { this.refs.zoomToFitButton.removeEventListener('click', zoomToFitClickHandler) }))

const wheelHandler = (event) => {
if (event.ctrlKey) {
const zoomOld = this.zoomFactor
if (event.wheelDeltaY>0) {
this.zoomFactor += 0.025
} else {
this.zoomFactor -= 0.025
}
this.zoomFactor = Math.round(Math.max(this.zoomFactor, 0.025)/0.025)*0.025
this.updateSize()
}
}
this.refs.imageContainer.addEventListener('wheel', wheelHandler)
this.disposables.add(new Disposable(() => { this.refs.imageContainer.removeEventListener('wheel', wheelHandler) }))
}

onDidLoad (callback) {
return this.emitter.on('did-load', callback)
}

update () {}
update() {}

destroy () {
destroy() {
this.disposables.dispose()
this.emitter.dispose()
etch.destroy(this)
}

render () {
render() {
return (
$.div({className: 'image-view', tabIndex: -1},
$.div({className: 'image-controls', ref: 'imageControls'},
Expand All @@ -127,19 +142,19 @@ class ImageEditorView {
)
),
$.div({className: 'image-controls-group btn-group'},
$.button({className: 'btn zoom-to-fit-button selected', ref: 'zoomToFitButton'},
$.button({className: 'btn zoom-to-fit-button', ref: 'zoomToFitButton'},
'Zoom to fit'
)
)
),
$.div({className: 'image-container zoom-to-fit', ref: 'imageContainer'},
$.div({className: 'image-container', ref: 'imageContainer'},
$.img({ref: 'image'})
)
)
)
}

updateImageURI () {
updateImageURI() {
this.refs.image.src = `${this.editor.getEncodedURI()}?time=${Date.now()}`
this.refs.image.onload = () => {
this.refs.image.onload = null
Expand All @@ -154,111 +169,102 @@ class ImageEditorView {
return this.emitter.on('did-update', callback)
}

// Zooms the image out by 25%.
zoomOut () {
this.percentageStep = Math.max(0, --this.percentageStep)
this.adjustSize(this.percentageStep)
zoomOut() {
for (let i = this.steps.length-1; i >= 0; i--) {
if (this.steps[i]<this.zoomFactor) {
this.zoomFactor = this.steps[i]
this.updateSize()
break
}
}
}

// Zooms the image in by 25%.
zoomIn () {
this.percentageStep = Math.min(this.steps.length - 1, ++this.percentageStep)
this.adjustSize(this.percentageStep)
zoomIn() {
for (let i = 0; i < this.steps.length; i++) {
if (this.steps[i]>this.zoomFactor) {
this.zoomFactor = this.steps[i]
this.updateSize()
break
}
}
}

// Zooms the image to its normal width and height.
resetZoom () {
resetZoom() {
if (!this.loaded || this.element.offsetHeight === 0) {
return
}

this.mode = 'reset-zoom'
this.refs.imageContainer.classList.remove('zoom-to-fit')
this.refs.zoomToFitButton.classList.remove('selected')
this.refs.image.style.width = this.originalWidth + 'px'
this.refs.image.style.height = this.originalHeight + 'px'
this.refs.resetZoomButton.textContent = '100%'
this.percentageStep = 4
this.zoomFactor = 1
this.updateSize()
}

// Zooms to fit the image, doesn't scale beyond actual size
zoomToFit () {
zoomToFit(zoomLimit) {
if (!this.loaded || this.element.offsetHeight === 0) {
return
}

this.mode = 'zoom-to-fit'
this.refs.imageContainer.classList.add('zoom-to-fit')
this.refs.zoomToFitButton.classList.add('selected')
this.refs.image.style.width = ''
this.refs.image.style.height = ''
this.refs.resetZoomButton.textContent = 'Auto'
this.percentageStep = 4
this.zoomFactor = Math.min(
this.refs.imageContainer.offsetWidth/this.refs.image.naturalWidth,
this.refs.imageContainer.offsetHeight/this.refs.image.naturalHeight,
)
if (zoomLimit) { this.zoomFactor = Math.min(this.zoomFactor, zoomLimit) }
this.updateSize()
}

// Adjust the size of the image by the given multiplying factor.
//
// factor - A {Number} to multiply against the current size.
adjustSize (percentageStep) {
updateSize() {
if (!this.loaded || this.element.offsetHeight === 0) {
return
}

if (this.mode === 'zoom-to-fit') {
this.mode = 'zoom-manual'
this.refs.imageContainer.classList.remove('zoom-to-fit')
this.refs.zoomToFitButton.classList.remove('selected')
} else if (this.mode === 'reset-zoom') {
this.mode = 'zoom-manual'
}

const factor = this.steps[percentageStep]
const newWidth = Math.round(this.originalWidth * factor)
const newHeight = Math.round(this.originalHeight * factor)
const percent = Math.max(1, Math.round((newWidth / this.originalWidth) * 100))

// Switch to pixelated rendering when image is bigger than 200%
if (newWidth > (this.originalWidth * 2)) {
this.refs.image.style.imageRendering = 'pixelated'
} else {
this.refs.image.style.imageRendering = ''
}
const newWidth = Math.round(this.refs.image.naturalWidth * this.zoomFactor)
const newHeight = Math.round(this.refs.image.naturalHeight * this.zoomFactor)
const percent = Math.round((newWidth / this.refs.image.naturalWidth) * 1000)/10

this.refs.image.style.width = newWidth + 'px'
this.refs.image.style.height = newHeight + 'px'
this.refs.resetZoomButton.textContent = percent + '%'

this.centerImage()
}

centerImage() {
this.refs.imageContainer.scrollTop = this.zoomFactor*this.refs.image.naturalHeight/2-this.refs.imageContainer.offsetHeight/2
this.refs.imageContainer.scrollLeft = this.zoomFactor*this.refs.image.naturalWidth/2-this.refs.imageContainer.offsetWidth/2
}

// Changes the background color of the image view.
//
// color - A {String} that gets used as class name.
changeBackground (color) {
if (this.loaded && this.element.offsetHeight > 0 && color) {
this.refs.imageContainer.setAttribute('background', color)
}
}

scrollUp () {
this.refs.imageContainer.scrollTop -= document.body.offsetHeight / 20
scrollUp() {
this.refs.imageContainer.scrollTop -= this.refs.imageContainer.offsetHeight / 20
}

scrollDown() {
this.refs.imageContainer.scrollTop += this.refs.imageContainer.offsetHeight / 20
}

scrollLeft() {
this.refs.imageContainer.scrollLeft -= this.refs.imageContainer.offsetWidth / 20
}

scrollDown () {
this.refs.imageContainer.scrollTop += document.body.offsetHeight / 20
scrollRight() {
this.refs.imageContainer.scrollLeft += this.refs.imageContainer.offsetWidth / 20
}

pageUp () {
pageUp() {
this.refs.imageContainer.scrollTop -= this.element.offsetHeight
}

pageDown () {
pageDown() {
this.refs.imageContainer.scrollTop += this.element.offsetHeight
}

scrollToTop () {
scrollToTop() {
this.refs.imageContainer.scrollTop = 0
}

scrollToBottom () {
scrollToBottom() {
this.refs.imageContainer.scrollTop = this.refs.imageContainer.scrollHeight
}
}
7 changes: 0 additions & 7 deletions packages/image-view/spec/image-editor-view-spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -101,13 +101,6 @@ describe('ImageEditorView', () => {
})
})

describe('.adjustSize(factor)', () => {
it('does not allow a zoom percentage lower than 10%', () => {
view.adjustSize(0)
expect(view.refs.resetZoomButton.textContent).toBe('10%')
})
})

describe('when special characters are used in the file name', () => {
describe("when '?' exists in the file name", () => {
it('is replaced with %3F', () => {
Expand Down
29 changes: 1 addition & 28 deletions packages/image-view/styles/image-view.less
Original file line number Diff line number Diff line change
Expand Up @@ -61,12 +61,6 @@
.reset-zoom-button {
min-width: 5em;
}

// disabled once the button is selected
.zoom-to-fit-button.selected {
pointer-events: none;
cursor: default;
}
}


Expand All @@ -78,7 +72,7 @@
overflow: auto;

img {
display: block !important;
display: block;
flex: none;
margin: auto;
}
Expand All @@ -94,25 +88,4 @@
background-image: url(@transparent-background-image);
}



// Zoom to fit -------------------
// Scales the image to fit the available space.

.zoom-to-fit {
&.image-container {
padding: @component-padding;

img {
flex: 1 1 0;
min-width: 0;
margin: 0;

// Alternative: object-fit: contain;
// then it would also scale larger than its original size
object-fit: scale-down;
}
}
}

}
Loading