Skip to content

Commit

Permalink
Implement asynchronous animated scrolling
Browse files Browse the repository at this point in the history
When both `scrollAnimation` and `independentMinimapScroll` settings are
enabled, the animation of the minimap no longer follow the animation of
the editor, preventing the minimap from jumping to the starting editor
scroll before moving towards the end scrolling position.
  • Loading branch information
abe33 committed Mar 14, 2016
1 parent 966eb29 commit 7477ed0
Show file tree
Hide file tree
Showing 3 changed files with 97 additions and 26 deletions.
42 changes: 29 additions & 13 deletions lib/minimap-element.js
Original file line number Diff line number Diff line change
Expand Up @@ -980,19 +980,34 @@ export default class MinimapElement {
* @access private
*/
canvasLeftMousePressed (y) {
let deltaY = y - this.getBoundingClientRect().top
let row = Math.floor(deltaY / this.minimap.getLineHeight()) + this.minimap.getFirstVisibleScreenRow()
const deltaY = y - this.getBoundingClientRect().top
const row = Math.floor(deltaY / this.minimap.getLineHeight()) + this.minimap.getFirstVisibleScreenRow()

let textEditor = this.minimap.getTextEditor()
const textEditor = this.minimap.getTextEditor()

let scrollTop = row * textEditor.getLineHeightInPixels() - this.minimap.getTextEditorHeight() / 2
const scrollTop = row * textEditor.getLineHeightInPixels() - this.minimap.getTextEditorHeight() / 2

if (atom.config.get('minimap.scrollAnimation')) {
const duration = atom.config.get('minimap.scrollAnimationDuration')
const independentScroll = this.minimap.scrollIndependentlyOnMouseWheel()

let from = this.minimap.getTextEditorScrollTop()
let to = scrollTop
let step = (now) => this.minimap.setTextEditorScrollTop(now)
let duration = atom.config.get('minimap.scrollAnimationDuration')
this.animate({from: from, to: to, duration: duration, step: step})
let step

if (independentScroll) {
const minimapFrom = this.minimap.getScrollTop()
const minimapTo = Math.min(1, scrollTop / (this.minimap.getTextEditorMaxScrollTop() || 1)) * this.minimap.getMaxScrollTop()

step = (now, t) => {
this.minimap.setTextEditorScrollTop(now, true)
this.minimap.setScrollTop(minimapFrom + (minimapTo - minimapFrom) * t)
}
this.animate({from: from, to: to, duration: duration, step: step})
} else {
step = (now) => this.minimap.setTextEditorScrollTop(now)
this.animate({from: from, to: to, duration: duration, step: step})
}
} else {
this.minimap.setTextEditorScrollTop(scrollTop)
}
Expand Down Expand Up @@ -1255,25 +1270,26 @@ export default class MinimapElement {
* @access private
*/
animate ({from, to, duration, step}) {
const start = this.getTime()
let progress
let start = this.getTime()

let swing = function (progress) {
const swing = function (progress) {
return 0.5 - Math.cos(progress * Math.PI) / 2
}

let update = () => {
const update = () => {
if (!this.minimap) { return }

let passed = this.getTime() - start
const passed = this.getTime() - start
if (duration === 0) {
progress = 1
} else {
progress = passed / duration
}
if (progress > 1) { progress = 1 }
let delta = swing(progress)
step(from + (to - from) * delta)
const delta = swing(progress)
const value = from + (to - from) * delta
step(value, delta)

if (progress < 1) { requestAnimationFrame(update) }
}
Expand Down
11 changes: 9 additions & 2 deletions lib/minimap.js
Original file line number Diff line number Diff line change
Expand Up @@ -205,10 +205,14 @@ export default class Minimap {
}))

subs.add(this.adapter.onDidChangeScrollTop(() => {
if (!this.standAlone) {
if (!this.standAlone && !this.ignoreTextEditorScroll) {
this.updateScrollTop()
this.emitter.emit('did-change-scroll-top', this)
}

if (this.ignoreTextEditorScroll) {
this.ignoreTextEditorScroll = false
}
}))
subs.add(this.adapter.onDidChangeScrollLeft(() => {
if (!this.standAlone) {
Expand Down Expand Up @@ -478,7 +482,10 @@ export default class Minimap {
*
* @param {number} scrollTop the new scroll top value
*/
setTextEditorScrollTop (scrollTop) { this.adapter.setScrollTop(scrollTop) }
setTextEditorScrollTop (scrollTop, ignoreTextEditorScroll = false) {
this.ignoreTextEditorScroll = ignoreTextEditorScroll
this.adapter.setScrollTop(scrollTop)
}

/**
* Returns the `TextEditor` scroll left value.
Expand Down
70 changes: 59 additions & 11 deletions spec/minimap-element-spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -660,6 +660,8 @@ describe('MinimapElement', () => {
})

describe('pressing the mouse on the minimap canvas (without scroll animation)', () => {
let canvas

beforeEach(() => {
let t = 0
spyOn(minimapElement, 'getTime').andCallFake(() => {
Expand All @@ -672,15 +674,29 @@ describe('MinimapElement', () => {
atom.config.set('minimap.scrollAnimation', false)

canvas = minimapElement.getFrontCanvas()
mousedown(canvas)
})

it('scrolls the editor to the line below the mouse', () => {
expect(editorElement.getScrollTop()).toBeGreaterThan(380)
mousedown(canvas)
expect(editorElement.getScrollTop()).toBeCloseTo(480)
})

describe('when independentMinimapScroll setting is enabled', () => {
beforeEach(() => {
minimap.setScrollTop(1000)
atom.config.set('minimap.independentMinimapScroll', true)
})

it('scrolls the editor to the line below the mouse', () => {
mousedown(canvas)
expect(editorElement.getScrollTop()).toBeCloseTo(480)
})
})
})

describe('pressing the mouse on the minimap canvas (with scroll animation)', () => {
let canvas

beforeEach(() => {
let t = 0
spyOn(minimapElement, 'getTime').andCallFake(() => {
Expand All @@ -694,27 +710,59 @@ describe('MinimapElement', () => {
atom.config.set('minimap.scrollAnimationDuration', 300)

canvas = minimapElement.getFrontCanvas()
mousedown(canvas)

waitsFor(() => { return nextAnimationFrame !== noAnimationFrame })
})

it('scrolls the editor gradually to the line below the mouse', () => {
mousedown(canvas)
waitsFor(() => { return nextAnimationFrame !== noAnimationFrame })
// wait until all animations run out
waitsFor(() => {
// Should be 400 on stable and 480 on beta.
// I'm still looking for a reason.
nextAnimationFrame !== noAnimationFrame && nextAnimationFrame()
return editorElement.getScrollTop() >= 380
return editorElement.getScrollTop() >= 480
})
})

it('stops the animation if the text editor is destroyed', () => {
editor.destroy()
mousedown(canvas)
waitsFor(() => { return nextAnimationFrame !== noAnimationFrame })

nextAnimationFrame !== noAnimationFrame && nextAnimationFrame()
runs(() => {
editor.destroy()

expect(nextAnimationFrame === noAnimationFrame)
nextAnimationFrame !== noAnimationFrame && nextAnimationFrame()

expect(nextAnimationFrame === noAnimationFrame)
})
})

describe('when independentMinimapScroll setting is enabled', () => {
beforeEach(() => {
minimap.setScrollTop(1000)
atom.config.set('minimap.independentMinimapScroll', true)
})

it('scrolls the editor gradually to the line below the mouse', () => {
mousedown(canvas)
waitsFor(() => { return nextAnimationFrame !== noAnimationFrame })
// wait until all animations run out
waitsFor(() => {
nextAnimationFrame !== noAnimationFrame && nextAnimationFrame()
return editorElement.getScrollTop() >= 480
})
})

it('stops the animation if the text editor is destroyed', () => {
mousedown(canvas)
waitsFor(() => { return nextAnimationFrame !== noAnimationFrame })

runs(() => {
editor.destroy()

nextAnimationFrame !== noAnimationFrame && nextAnimationFrame()

expect(nextAnimationFrame === noAnimationFrame)
})
})
})
})

Expand Down

0 comments on commit 7477ed0

Please sign in to comment.