提交 ac99d550 编写于 作者: A Alex Dima

* switch to using ease-out-cubic in smooth scrolling

* use `dom.scheduleAtNextAnimationFrame`
上级 b34a8280
......@@ -374,7 +374,7 @@ export class ScrollableElement extends AbstractScrollableElement {
constructor(element: HTMLElement, options: ScrollableElementCreationOptions) {
options = options || {};
options.mouseWheelSmoothScroll = false;
const scrollable = new Scrollable(0);
const scrollable = new Scrollable(0, (callback) => DomUtils.scheduleAtNextAnimationFrame(callback));
super(element, options, scrollable);
this._register(scrollable);
}
......
......@@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------------*/
'use strict';
import { Disposable } from 'vs/base/common/lifecycle';
import { Disposable, IDisposable } from 'vs/base/common/lifecycle';
import Event, { Emitter } from 'vs/base/common/event';
export enum ScrollbarVisibility {
......@@ -174,16 +174,18 @@ export class Scrollable extends Disposable {
_scrollableBrand: void;
private readonly _smoothScrollDuration: number;
private readonly _scheduleAtNextAnimationFrame: (callback: () => void) => IDisposable;
private _state: ScrollState;
private _smoothScrolling: SmoothScrollingOperation;
private _onScroll = this._register(new Emitter<ScrollEvent>());
public onScroll: Event<ScrollEvent> = this._onScroll.event;
constructor(smoothScrollDuration: number) {
constructor(smoothScrollDuration: number, scheduleAtNextAnimationFrame: (callback: () => void) => IDisposable) {
super();
this._smoothScrollDuration = smoothScrollDuration;
this._scheduleAtNextAnimationFrame = scheduleAtNextAnimationFrame;
this._state = new ScrollState(0, 0, 0, 0, 0, 0);
this._smoothScrolling = null;
}
......@@ -253,21 +255,33 @@ export class Scrollable extends Disposable {
}
if (this._smoothScrolling) {
const oldSmoothScrolling = this._smoothScrolling;
const newSmoothScrolling = oldSmoothScrolling.combine(this._state, update, this._smoothScrollDuration);
if (oldSmoothScrolling.softEquals(newSmoothScrolling)) {
// No change
// Combine our pending scrollLeft/scrollTop with incoming scrollLeft/scrollTop
update = {
scrollLeft: (typeof update.scrollLeft === 'undefined' ? this._smoothScrolling.to.scrollLeft : update.scrollLeft),
scrollTop: (typeof update.scrollTop === 'undefined' ? this._smoothScrolling.to.scrollTop : update.scrollTop)
};
// Validate `update`
const validTarget = this._state.withScrollPosition(update);
if (this._smoothScrolling.to.scrollLeft === validTarget.scrollLeft && this._smoothScrolling.to.scrollTop === validTarget.scrollTop) {
// No need to interrupt or extend the current animation since we're going to the same place
return;
}
oldSmoothScrolling.dispose();
const newSmoothScrolling = this._smoothScrolling.combine(this._state, validTarget, this._smoothScrollDuration);
this._smoothScrolling.dispose();
this._smoothScrolling = newSmoothScrolling;
} else {
this._smoothScrolling = SmoothScrollingOperation.start(this._state, update, this._smoothScrollDuration);
// Validate `update`
const validTarget = this._state.withScrollPosition(update);
this._smoothScrolling = SmoothScrollingOperation.start(this._state, validTarget, this._smoothScrollDuration);
}
// Begin smooth scrolling animation
this._smoothScrolling.animationFrameToken = requestAnimationFrame(() => {
this._smoothScrolling.animationFrameToken = -1;
this._smoothScrolling.animationFrameDisposable = this._scheduleAtNextAnimationFrame(() => {
this._smoothScrolling.animationFrameDisposable = null;
this._performSmoothScrolling();
});
}
......@@ -278,13 +292,17 @@ export class Scrollable extends Disposable {
this._setState(newState);
if (!update.isDone) {
// Continue smooth scrolling animation
this._smoothScrolling.animationFrameToken = requestAnimationFrame(() => {
this._smoothScrolling.animationFrameToken = -1;
this._performSmoothScrolling();
});
if (update.isDone) {
this._smoothScrolling.dispose();
this._smoothScrolling = null;
return;
}
// Continue smooth scrolling animation
this._smoothScrolling.animationFrameDisposable = this._scheduleAtNextAnimationFrame(() => {
this._smoothScrolling.animationFrameDisposable = null;
this._performSmoothScrolling();
});
}
private _setState(newState: ScrollState): void {
......@@ -318,27 +336,20 @@ class SmoothScrollingOperation {
public to: IScrollPosition;
public readonly duration: number;
private readonly _startTime: number;
public animationFrameToken: number;
public animationFrameDisposable: IDisposable;
private constructor(from: IScrollPosition, to: IScrollPosition, startTime: number, duration: number) {
this.from = from;
this.to = to;
this.duration = duration;
this._startTime = startTime;
this.animationFrameToken = -1;
}
public softEquals(other: SmoothScrollingOperation): boolean {
return (
this.to.scrollLeft === other.to.scrollLeft
&& this.to.scrollTop === other.to.scrollTop
);
this.animationFrameDisposable = null;
}
public dispose(): void {
if (this.animationFrameToken !== -1) {
cancelAnimationFrame(this.animationFrameToken);
this.animationFrameToken = -1;
if (this.animationFrameDisposable !== null) {
this.animationFrameDisposable.dispose();
this.animationFrameDisposable = null;
}
}
......@@ -350,7 +361,7 @@ class SmoothScrollingOperation {
const completion = (Date.now() - this._startTime) / this.duration;
if (completion < 1) {
const t = easeInOutCubic(completion);
const t = easeOutCubic(completion);
const newScrollLeft = this.from.scrollLeft + (this.to.scrollLeft - this.from.scrollLeft) * t;
const newScrollTop = this.from.scrollTop + (this.to.scrollTop - this.from.scrollTop) * t;
return new SmoothScrollingUpdate(newScrollLeft, newScrollTop, false);
......@@ -359,26 +370,12 @@ class SmoothScrollingOperation {
return new SmoothScrollingUpdate(this.to.scrollLeft, this.to.scrollTop, true);
}
public combine(from: ScrollState, to: INewScrollPosition, duration: number): SmoothScrollingOperation {
// Combine our scrollLeft/scrollTop with incoming scrollLeft/scrollTop
to = {
scrollLeft: (typeof to.scrollLeft === 'undefined' ? this.to.scrollLeft : to.scrollLeft),
scrollTop: (typeof to.scrollTop === 'undefined' ? this.to.scrollTop : to.scrollTop)
};
// Validate `to`
const validTarget = from.withScrollPosition(to);
// TODO@smooth: This is our opportunity to combine animations
return new SmoothScrollingOperation(from, validTarget, Date.now(), duration);
public combine(from: IScrollPosition, to: IScrollPosition, duration: number): SmoothScrollingOperation {
return SmoothScrollingOperation.start(from, to, duration);
}
public static start(from: ScrollState, to: INewScrollPosition, duration: number): SmoothScrollingOperation {
// Validate `to`
const validTarget = from.withScrollPosition(to);
return new SmoothScrollingOperation(from, validTarget, Date.now(), duration);
public static start(from: IScrollPosition, to: IScrollPosition, duration: number): SmoothScrollingOperation {
return new SmoothScrollingOperation(from, to, Date.now(), duration);
}
}
......@@ -386,9 +383,6 @@ function easeInCubic(t) {
return Math.pow(t, 3);
}
function easeInOutCubic(t) {
if (t < 0.5) {
return easeInCubic(t * 2) / 2;
}
return 1 - easeInCubic((1 - t) * 2) / 2;
function easeOutCubic(t) {
return 1 - easeInCubic(1 - t);
}
......@@ -21,7 +21,7 @@ import { ICodeEditorService } from 'vs/editor/common/services/codeEditorService'
import { Configuration } from 'vs/editor/browser/config/configuration';
import * as editorBrowser from 'vs/editor/browser/editorBrowser';
import { View, IOverlayWidgetData, IContentWidgetData } from 'vs/editor/browser/view/viewImpl';
import { Disposable } from 'vs/base/common/lifecycle';
import { Disposable, IDisposable } from 'vs/base/common/lifecycle';
import Event, { Emitter } from 'vs/base/common/event';
import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent';
import { InternalEditorAction } from 'vs/editor/common/editorAction';
......@@ -403,6 +403,10 @@ export abstract class CodeEditorWidget extends CommonCodeEditor implements edito
}
}
protected _scheduleAtNextAnimationFrame(callback: () => void): IDisposable {
return dom.scheduleAtNextAnimationFrame(callback);
}
protected _createView(): void {
this._view = new View(
this._commandService,
......
......@@ -862,7 +862,7 @@ export abstract class CommonCodeEditor extends Disposable implements editorCommo
this.model.onBeforeAttached();
this.viewModel = new ViewModel(this.id, this._configuration, this.model);
this.viewModel = new ViewModel(this.id, this._configuration, this.model, (callback) => this._scheduleAtNextAnimationFrame(callback));
this.listenersToRemove.push(this.model.addBulkListener((events) => {
for (let i = 0, len = events.length; i < len; i++) {
......@@ -935,6 +935,7 @@ export abstract class CommonCodeEditor extends Disposable implements editorCommo
}
}
protected abstract _scheduleAtNextAnimationFrame(callback: () => void): IDisposable;
protected abstract _createView(): void;
protected _postDetachModelCleanup(detachedModel: editorCommon.IModel): void {
......
......@@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------------*/
'use strict';
import { Disposable } from 'vs/base/common/lifecycle';
import { Disposable, IDisposable } from 'vs/base/common/lifecycle';
import { Scrollable, ScrollEvent, ScrollbarVisibility, IScrollDimensions } from 'vs/base/common/scrollable';
import * as editorCommon from 'vs/editor/common/editorCommon';
import { LinesLayout } from 'vs/editor/common/viewLayout/linesLayout';
......@@ -24,14 +24,14 @@ export class ViewLayout extends Disposable implements IViewLayout {
public readonly scrollable: Scrollable;
public readonly onDidScroll: Event<ScrollEvent>;
constructor(configuration: editorCommon.IConfiguration, lineCount: number) {
constructor(configuration: editorCommon.IConfiguration, lineCount: number, scheduleAtNextAnimationFrame: (callback: () => void) => IDisposable) {
super();
this._configuration = configuration;
this._linesLayout = new LinesLayout(lineCount, this._configuration.editor.lineHeight);
// TODO@smooth: [MUST] have an editor option for smooth scrolling
this.scrollable = this._register(new Scrollable(125));
this.scrollable = this._register(new Scrollable(125, scheduleAtNextAnimationFrame));
this.scrollable.setScrollDimensions({
width: configuration.editor.layoutInfo.contentWidth,
height: configuration.editor.layoutInfo.contentHeight
......
......@@ -21,6 +21,7 @@ import { IConfigurationChangedEvent } from 'vs/editor/common/config/editorOption
import { CharacterHardWrappingLineMapperFactory } from 'vs/editor/common/viewModel/characterHardWrappingLineMapper';
import { ViewLayout } from 'vs/editor/common/viewLayout/viewLayout';
import { Color } from 'vs/base/common/color';
import { IDisposable } from "vs/base/common/lifecycle";
const USE_IDENTITY_LINES_COLLECTION = true;
......@@ -38,7 +39,7 @@ export class ViewModel extends viewEvents.ViewEventEmitter implements IViewModel
private _isDisposing: boolean;
private _centeredViewLine: number;
constructor(editorId: number, configuration: editorCommon.IConfiguration, model: editorCommon.IModel) {
constructor(editorId: number, configuration: editorCommon.IConfiguration, model: editorCommon.IModel, scheduleAtNextAnimationFrame: (callback: () => void) => IDisposable) {
super();
this.editorId = editorId;
......@@ -70,7 +71,7 @@ export class ViewModel extends viewEvents.ViewEventEmitter implements IViewModel
this.coordinatesConverter = this.lines.createCoordinatesConverter();
this.viewLayout = this._register(new ViewLayout(this.configuration, this.getLineCount()));
this.viewLayout = this._register(new ViewLayout(this.configuration, this.getLineCount(), scheduleAtNextAnimationFrame));
this._register(this.viewLayout.onDidScroll((e) => {
this._emit([new viewEvents.ViewScrollChangedEvent(e)]);
......
......@@ -152,7 +152,7 @@ suite('Editor Controller - Cursor', () => {
thisModel = Model.createFromString(text);
thisConfiguration = new TestConfiguration(null);
thisViewModel = new ViewModel(0, thisConfiguration, thisModel);
thisViewModel = new ViewModel(0, thisConfiguration, thisModel, null);
thisCursor = new Cursor(thisConfiguration, thisModel, thisViewModel);
});
......@@ -736,7 +736,7 @@ suite('Editor Controller - Cursor', () => {
'var newer = require("gulp-newer");',
].join('\n'));
const config = new TestConfiguration(null);
const viewModel = new ViewModel(0, config, model);
const viewModel = new ViewModel(0, config, model, null);
const cursor = new Cursor(config, model, viewModel);
moveTo(cursor, 1, 4, false);
......@@ -775,7 +775,7 @@ suite('Editor Controller - Cursor', () => {
'<property id="SomeThing" key="SomeKey" value="00X"/>',
].join('\n'));
const config = new TestConfiguration(null);
const viewModel = new ViewModel(0, config, model);
const viewModel = new ViewModel(0, config, model, null);
const cursor = new Cursor(config, model, viewModel);
moveTo(cursor, 10, 10, false);
......@@ -837,7 +837,7 @@ suite('Editor Controller - Cursor', () => {
'<property id="SomeThing" key="SomeKey" value="00X"/>',
].join('\n'));
const config = new TestConfiguration(null);
const viewModel = new ViewModel(0, config, model);
const viewModel = new ViewModel(0, config, model, null);
const cursor = new Cursor(config, model, viewModel);
moveTo(cursor, 10, 10, false);
......@@ -886,7 +886,7 @@ suite('Editor Controller - Cursor', () => {
'var newer = require("gulp-newer");',
].join('\n'));
const config = new TestConfiguration(null);
const viewModel = new ViewModel(0, config, model);
const viewModel = new ViewModel(0, config, model, null);
const cursor = new Cursor(config, model, viewModel);
moveTo(cursor, 1, 4, false);
......@@ -3118,7 +3118,7 @@ function usingCursor(opts: ICursorOpts, callback: (model: Model, cursor: Cursor)
let model = Model.createFromString(opts.text.join('\n'), opts.modelOpts, opts.languageIdentifier);
model.forceTokenization(model.getLineCount());
let config = new TestConfiguration(opts.editorOpts);
let viewModel = new ViewModel(0, config, model);
let viewModel = new ViewModel(0, config, model, null);
let cursor = new Cursor(config, model, viewModel);
callback(model, cursor);
......
......@@ -33,7 +33,7 @@ suite('Cursor move command test', () => {
thisModel = Model.createFromString(text);
thisConfiguration = new TestConfiguration(null);
thisViewModel = new ViewModel(0, thisConfiguration, thisModel);
thisViewModel = new ViewModel(0, thisConfiguration, thisModel, null);
thisCursor = new Cursor(thisConfiguration, thisModel, thisViewModel);
});
......
......@@ -15,6 +15,7 @@ import * as editorCommon from 'vs/editor/common/editorCommon';
import { Model } from 'vs/editor/common/model/model';
import { TestConfiguration } from 'vs/editor/test/common/mocks/testConfiguration';
import * as editorOptions from 'vs/editor/common/config/editorOptions';
import { IDisposable } from "vs/base/common/lifecycle";
export class MockCodeEditor extends CommonCodeEditor {
protected _createConfiguration(options: editorOptions.IEditorOptions): CommonEditorConfiguration {
......@@ -28,6 +29,7 @@ export class MockCodeEditor extends CommonCodeEditor {
public hasWidgetFocus(): boolean { return true; };
protected _enableEmptySelectionClipboard(): boolean { return false; }
protected _scheduleAtNextAnimationFrame(callback: () => void): IDisposable { throw new Error('Notimplemented'); }
protected _createView(): void { }
protected _registerDecorationType(key: string, options: editorCommon.IDecorationRenderOptions, parentTypeKey?: string): void { throw new Error('NotImplemented'); }
......
......@@ -16,7 +16,7 @@ export function testViewModel(text: string[], options: MockCodeEditorCreationOpt
let model = Model.createFromString(text.join('\n'));
let viewModel = new ViewModel(EDITOR_ID, configuration, model);
let viewModel = new ViewModel(EDITOR_ID, configuration, model, null);
callback(viewModel, model);
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册