Skip to content

Commit

Permalink
[tree-view] Zoom to mouse fixed
Browse files Browse the repository at this point in the history
  • Loading branch information
asiloisad committed Jan 16, 2025
1 parent b53787f commit 29e6012
Show file tree
Hide file tree
Showing 4 changed files with 125 additions and 75 deletions.
3 changes: 3 additions & 0 deletions packages/image-view/keymaps/image-view.cson
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
'cmd-=': 'image-view:zoom-in'
'cmd--': 'image-view:zoom-out'
'cmd-_': 'image-view:zoom-out'
'cmd-8': 'image-view:center'
'cmd-9': 'image-view:zoom-to-fit'
'cmd-0': 'image-view:reset-zoom'

Expand All @@ -11,6 +12,7 @@
'ctrl-=': 'image-view:zoom-in'
'ctrl--': 'image-view:zoom-out'
'ctrl-_': 'image-view:zoom-out'
'ctrl-8': 'image-view:center'
'ctrl-9': 'image-view:zoom-to-fit'
'ctrl-0': 'image-view:reset-zoom'

Expand All @@ -19,5 +21,6 @@
'ctrl-=': 'image-view:zoom-in'
'ctrl--': 'image-view:zoom-out'
'ctrl-_': 'image-view:zoom-out'
'ctrl-8': 'image-view:center'
'ctrl-9': 'image-view:zoom-to-fit'
'ctrl-0': 'image-view:reset-zoom'
187 changes: 113 additions & 74 deletions packages/image-view/lib/image-editor-view.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,14 @@ class ImageEditorView {
this.disposables = new CompositeDisposable()
this.imageSize = fs.statSync(this.editor.getPath()).size
this.loaded = false
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
this.levels = [0.05, 0.1, 0.25, 0.5, 0.75, 1.0, 1.25, 1.5, 2, 3, 4, 5, 7.5, 10]
this.zoom = 1.00 ; this.step = null ; this.auto = false
etch.initialize(this)

this.defaultBackgroundColor = atom.config.get('image-view.defaultBackgroundColor')
this.refs.imageContainer.setAttribute('background', this.defaultBackgroundColor)

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

this.disposables.add(this.editor.onDidChange(() => this.updateImageURI()))
Expand All @@ -24,6 +28,7 @@ class ImageEditorView {
'image-view:zoom-out': () => this.zoomOut(),
'image-view:reset-zoom': () => this.resetZoom(),
'image-view:zoom-to-fit': () => this.zoomToFit(),
'image-view:center': () => this.centerImage(),
'core:move-up': () => this.scrollUp(),
'core:move-down': () => this.scrollDown(),
'core:move-left': () => this.scrollLeft(),
Expand All @@ -34,21 +39,10 @@ class ImageEditorView {
'core:move-to-bottom': () => this.scrollToBottom()
}))

this.refs.image.onload = () => {
this.refs.image.onload = null
this.originalHeight = this.refs.image.naturalHeight
this.originalWidth = this.refs.image.naturalWidth
this.loaded = true
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')
}

this.disposables.add(atom.tooltips.add(this.refs.whiteTransparentBackgroundButton, {title: 'Use white transparent background'}))
this.disposables.add(atom.tooltips.add(this.refs.blackTransparentBackgroundButton, {title: 'Use black transparent background'}))
this.disposables.add(atom.tooltips.add(this.refs.transparentTransparentBackgroundButton, {title: 'Use transparent background'}))
this.disposables.add(atom.tooltips.add(this.refs.whiteTransparentBackgroundButton, { title: 'Use white transparent background' }))
this.disposables.add(atom.tooltips.add(this.refs.blackTransparentBackgroundButton, { title: 'Use black transparent background' }))
this.disposables.add(atom.tooltips.add(this.refs.transparentTransparentBackgroundButton, { title: 'Use transparent background' }))
this.disposables.add(atom.tooltips.add(this.refs.nativeBackgroundButton, { title: 'Use native background' }))

const clickHandler = (event) => {
event.preventDefault()
Expand All @@ -62,6 +56,8 @@ class ImageEditorView {
this.disposables.add(new Disposable(() => { this.refs.blackTransparentBackgroundButton.removeEventListener('click', clickHandler) }))
this.refs.transparentTransparentBackgroundButton.addEventListener('click', clickHandler)
this.disposables.add(new Disposable(() => { this.refs.transparentTransparentBackgroundButton.removeEventListener('click', clickHandler) }))
this.refs.nativeBackgroundButton.addEventListener('click', clickHandler)
this.disposables.add(new Disposable(() => { this.refs.nativeBackgroundButton.removeEventListener('click', clickHandler) }))

const zoomInClickHandler = () => {
this.zoomIn()
Expand All @@ -81,26 +77,44 @@ class ImageEditorView {
this.refs.resetZoomButton.addEventListener('click', resetZoomClickHandler)
this.disposables.add(new Disposable(() => { this.refs.resetZoomButton.removeEventListener('click', resetZoomClickHandler) }))

const centerClickHandler = () => {
this.centerImage()
}
this.refs.centerButton.addEventListener('click', centerClickHandler)
this.disposables.add(new Disposable(() => { this.refs.centerButton.removeEventListener('click', centerClickHandler) }))

const zoomToFitClickHandler = () => {
this.zoomToFit()
}
this.refs.zoomToFitButton.addEventListener('click', zoomToFitClickHandler)
this.disposables.add(new Disposable(() => { this.refs.zoomToFitButton.removeEventListener('click', zoomToFitClickHandler) }))

const wheelHandler = (event) => {
const wheelContainerHandler = (event) => {
if (event.ctrlKey) {
event.stopPropagation()
const factor = event.wheelDeltaY>0 ? 1.2/1 : 1/1.2
this.zoomToCenterPoint(factor*this.zoom)
}
}
this.refs.imageContainer.addEventListener('wheel', wheelContainerHandler)
this.disposables.add(new Disposable(() => { this.refs.imageContainer.removeEventListener('wheel', wheelContainerHandler) }))

const wheelImageHandler = (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()
event.stopPropagation()
const factor = event.wheelDeltaY>0 ? 1.2/1 : 1/1.2
this.zoomToMousePosition(factor*this.zoom, event)
}
}
this.refs.imageContainer.addEventListener('wheel', wheelHandler)
this.disposables.add(new Disposable(() => { this.refs.imageContainer.removeEventListener('wheel', wheelHandler) }))
this.refs.image.addEventListener('wheel', wheelImageHandler)
this.disposables.add(new Disposable(() => { this.refs.image.removeEventListener('wheel', wheelImageHandler) }))

this.resizeObserver = new ResizeObserver(() => {
if (this.auto) {
this.zoomToFit()
}
})
this.resizeObserver.observe(this.refs.imageContainer)
}

onDidLoad (callback) {
Expand All @@ -112,6 +126,7 @@ class ImageEditorView {
destroy() {
this.disposables.dispose()
this.emitter.dispose()
this.resizeObserver.disconnect()
etch.destroy(this)
}

Expand All @@ -128,6 +143,9 @@ class ImageEditorView {
),
$.a({className: 'image-controls-color-transparent', value: 'transparent', ref: 'transparentTransparentBackgroundButton'},
'transparent'
),
$.a({className: 'image-controls-color-native', value: 'native', ref: 'nativeBackgroundButton'},
'native'
)
),
$.div({className: 'image-controls-group btn-group'},
Expand All @@ -142,6 +160,9 @@ class ImageEditorView {
)
),
$.div({className: 'image-controls-group btn-group'},
$.button({className: 'btn center-button', ref: 'centerButton'},
'Center'
),
$.button({className: 'btn zoom-to-fit-button', ref: 'zoomToFitButton'},
'Zoom to fit'
)
Expand All @@ -161,73 +182,91 @@ class ImageEditorView {
this.originalHeight = this.refs.image.naturalHeight
this.originalWidth = this.refs.image.naturalWidth
this.imageSize = fs.statSync(this.editor.getPath()).size
this.loaded = true
this.zoomToFit(1)
this.refs.image.style.display = ''
this.emitter.emit('did-update')
this.emitter.emit('did-load')
}
}

onDidUpdate (callback) {
return this.emitter.on('did-update', callback)
}

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
}
updateSize(zoom) {
if (!this.loaded || this.element.offsetHeight === 0) {
return
}
this.auto = false
this.refs.zoomToFitButton.classList.remove('selected')
const prev = this.zoom
this.zoom = Math.min(Math.max(zoom, 0.001), 100)
this.step = this.zoom/prev
const newWidth = Math.round(this.refs.image.naturalWidth * this.zoom)
const newHeight = Math.round(this.refs.image.naturalHeight * this.zoom)
const percent = Math.round(this.zoom * 1000) / 10
this.refs.image.style.width = newWidth + 'px'
this.refs.image.style.height = newHeight + 'px'
this.refs.resetZoomButton.textContent = percent + '%'
}

zoomIn() {
for (let i = 0; i < this.steps.length; i++) {
if (this.steps[i]>this.zoomFactor) {
this.zoomFactor = this.steps[i]
this.updateSize()
break
}
}
centerImage() {
this.refs.imageContainer.scrollTop = this.zoom * this.refs.image.naturalHeight / 2 - this.refs.imageContainer.offsetHeight / 2
this.refs.imageContainer.scrollLeft = this.zoom *this.refs.image.naturalWidth / 2 - this.refs.imageContainer.offsetWidth / 2
}

resetZoom() {
if (!this.loaded || this.element.offsetHeight === 0) {
return
}
this.zoomFactor = 1
this.updateSize()
zoomToMousePosition(zoom, event) {
this.updateSize(zoom)
this.refs.imageContainer.scrollLeft = this.step * event.offsetX - event.layerX
this.refs.imageContainer.scrollTop = this.step * event.offsetY - (event.layerY - this.refs.imageControls.offsetHeight)
}

zoomToCenterPoint(zoom) {
const coorX = this.refs.imageContainer.scrollLeft + this.refs.imageContainer.offsetWidth / 2
const coorY = this.refs.imageContainer.scrollTop + this.refs.imageContainer.offsetHeight / 2
this.updateSize(zoom)
this.refs.imageContainer.scrollLeft = this.step * coorX - this.refs.imageContainer.offsetWidth / 2
this.refs.imageContainer.scrollTop = this.step * coorY - this.refs.imageContainer.offsetHeight / 2
}

zoomToFit(zoomLimit) {
zoomToFit(limit) {
if (!this.loaded || this.element.offsetHeight === 0) {
return
}
this.zoomFactor = Math.min(
this.refs.imageContainer.offsetWidth/this.refs.image.naturalWidth,
this.refs.imageContainer.offsetHeight/this.refs.image.naturalHeight,
let zoom = 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()
if (limit) { zoom = Math.min(zoom, limit) }
this.updateSize(zoom)
this.auto = true
this.refs.zoomToFitButton.classList.add('selected')
}

updateSize() {
if (!this.loaded || this.element.offsetHeight === 0) {
return
zoomOut() {
for (let i = this.levels.length-1; i >= 0; i--) {
if (this.levels[i]<this.zoom) {
this.zoomToCenterPoint(this.levels[i])
break
}
}
}

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()
zoomIn() {
for (let i = 0; i < this.levels.length; i++) {
if (this.levels[i]>this.zoom) {
this.zoomToCenterPoint(this.levels[i])
break
}
}
}

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
resetZoom() {
if (!this.loaded || this.element.offsetHeight === 0) {
return
}
this.zoomToCenterPoint(1)
}

changeBackground (color) {
Expand All @@ -237,19 +276,19 @@ class ImageEditorView {
}

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

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

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

scrollRight() {
this.refs.imageContainer.scrollLeft += this.refs.imageContainer.offsetWidth / 20
this.refs.imageContainer.scrollLeft += this.refs.imageContainer.offsetWidth / 10
}

pageUp() {
Expand Down
2 changes: 1 addition & 1 deletion packages/image-view/lib/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ module.exports = {
config: {
defaultBackgroundColor: {
type: 'string',
enum: ['white', 'black', 'transparent'],
enum: ['white', 'black', 'transparent', 'native'],
default: 'transparent'
}
},
Expand Down
8 changes: 8 additions & 0 deletions packages/image-view/styles/image-view.less
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,10 @@
background-image: url(@transparent-background-image);
}

&-color-native {
background-color: @app-background-color;
}

.btn-group {
margin: @spacing;
}
Expand All @@ -75,6 +79,7 @@
display: block;
flex: none;
margin: auto;
-webkit-user-drag: none;
}
}

Expand All @@ -87,5 +92,8 @@
background-color: black;
background-image: url(@transparent-background-image);
}
[background="native"] {
background-color: @app-background-color;
}

}

0 comments on commit 29e6012

Please sign in to comment.