| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529 |
- /*---------------------------------------------------------------------------------------------
- * Copyright (c) Microsoft Corporation. All rights reserved.
- * Licensed under the MIT License. See License.txt in the project root for license information.
- *--------------------------------------------------------------------------------------------*/
- import { getZoomFactor } from '../../browser.js';
- import * as dom from '../../dom.js';
- import { createFastDomNode } from '../../fastDomNode.js';
- import { StandardWheelEvent } from '../../mouseEvent.js';
- import { HorizontalScrollbar } from './horizontalScrollbar.js';
- import { VerticalScrollbar } from './verticalScrollbar.js';
- import { Widget } from '../widget.js';
- import { TimeoutTimer } from '../../../common/async.js';
- import { Emitter } from '../../../common/event.js';
- import { dispose } from '../../../common/lifecycle.js';
- import * as platform from '../../../common/platform.js';
- import { Scrollable } from '../../../common/scrollable.js';
- import './media/scrollbars.css';
- const HIDE_TIMEOUT = 500;
- const SCROLL_WHEEL_SENSITIVITY = 50;
- const SCROLL_WHEEL_SMOOTH_SCROLL_ENABLED = true;
- class MouseWheelClassifierItem {
- constructor(timestamp, deltaX, deltaY) {
- this.timestamp = timestamp;
- this.deltaX = deltaX;
- this.deltaY = deltaY;
- this.score = 0;
- }
- }
- export class MouseWheelClassifier {
- constructor() {
- this._capacity = 5;
- this._memory = [];
- this._front = -1;
- this._rear = -1;
- }
- isPhysicalMouseWheel() {
- if (this._front === -1 && this._rear === -1) {
- // no elements
- return false;
- }
- // 0.5 * last + 0.25 * 2nd last + 0.125 * 3rd last + ...
- let remainingInfluence = 1;
- let score = 0;
- let iteration = 1;
- let index = this._rear;
- do {
- const influence = (index === this._front ? remainingInfluence : Math.pow(2, -iteration));
- remainingInfluence -= influence;
- score += this._memory[index].score * influence;
- if (index === this._front) {
- break;
- }
- index = (this._capacity + index - 1) % this._capacity;
- iteration++;
- } while (true);
- return (score <= 0.5);
- }
- accept(timestamp, deltaX, deltaY) {
- const item = new MouseWheelClassifierItem(timestamp, deltaX, deltaY);
- item.score = this._computeScore(item);
- if (this._front === -1 && this._rear === -1) {
- this._memory[0] = item;
- this._front = 0;
- this._rear = 0;
- }
- else {
- this._rear = (this._rear + 1) % this._capacity;
- if (this._rear === this._front) {
- // Drop oldest
- this._front = (this._front + 1) % this._capacity;
- }
- this._memory[this._rear] = item;
- }
- }
- /**
- * A score between 0 and 1 for `item`.
- * - a score towards 0 indicates that the source appears to be a physical mouse wheel
- * - a score towards 1 indicates that the source appears to be a touchpad or magic mouse, etc.
- */
- _computeScore(item) {
- if (Math.abs(item.deltaX) > 0 && Math.abs(item.deltaY) > 0) {
- // both axes exercised => definitely not a physical mouse wheel
- return 1;
- }
- let score = 0.5;
- const prev = (this._front === -1 && this._rear === -1 ? null : this._memory[this._rear]);
- if (prev) {
- // const deltaT = item.timestamp - prev.timestamp;
- // if (deltaT < 1000 / 30) {
- // // sooner than X times per second => indicator that this is not a physical mouse wheel
- // score += 0.25;
- // }
- // if (item.deltaX === prev.deltaX && item.deltaY === prev.deltaY) {
- // // equal amplitude => indicator that this is a physical mouse wheel
- // score -= 0.25;
- // }
- }
- if (!this._isAlmostInt(item.deltaX) || !this._isAlmostInt(item.deltaY)) {
- // non-integer deltas => indicator that this is not a physical mouse wheel
- score += 0.25;
- }
- return Math.min(Math.max(score, 0), 1);
- }
- _isAlmostInt(value) {
- const delta = Math.abs(Math.round(value) - value);
- return (delta < 0.01);
- }
- }
- MouseWheelClassifier.INSTANCE = new MouseWheelClassifier();
- export class AbstractScrollableElement extends Widget {
- constructor(element, options, scrollable) {
- super();
- this._onScroll = this._register(new Emitter());
- this.onScroll = this._onScroll.event;
- this._onWillScroll = this._register(new Emitter());
- element.style.overflow = 'hidden';
- this._options = resolveOptions(options);
- this._scrollable = scrollable;
- this._register(this._scrollable.onScroll((e) => {
- this._onWillScroll.fire(e);
- this._onDidScroll(e);
- this._onScroll.fire(e);
- }));
- const scrollbarHost = {
- onMouseWheel: (mouseWheelEvent) => this._onMouseWheel(mouseWheelEvent),
- onDragStart: () => this._onDragStart(),
- onDragEnd: () => this._onDragEnd(),
- };
- this._verticalScrollbar = this._register(new VerticalScrollbar(this._scrollable, this._options, scrollbarHost));
- this._horizontalScrollbar = this._register(new HorizontalScrollbar(this._scrollable, this._options, scrollbarHost));
- this._domNode = document.createElement('div');
- this._domNode.className = 'monaco-scrollable-element ' + this._options.className;
- this._domNode.setAttribute('role', 'presentation');
- this._domNode.style.position = 'relative';
- this._domNode.style.overflow = 'hidden';
- this._domNode.appendChild(element);
- this._domNode.appendChild(this._horizontalScrollbar.domNode.domNode);
- this._domNode.appendChild(this._verticalScrollbar.domNode.domNode);
- if (this._options.useShadows) {
- this._leftShadowDomNode = createFastDomNode(document.createElement('div'));
- this._leftShadowDomNode.setClassName('shadow');
- this._domNode.appendChild(this._leftShadowDomNode.domNode);
- this._topShadowDomNode = createFastDomNode(document.createElement('div'));
- this._topShadowDomNode.setClassName('shadow');
- this._domNode.appendChild(this._topShadowDomNode.domNode);
- this._topLeftShadowDomNode = createFastDomNode(document.createElement('div'));
- this._topLeftShadowDomNode.setClassName('shadow');
- this._domNode.appendChild(this._topLeftShadowDomNode.domNode);
- }
- else {
- this._leftShadowDomNode = null;
- this._topShadowDomNode = null;
- this._topLeftShadowDomNode = null;
- }
- this._listenOnDomNode = this._options.listenOnDomNode || this._domNode;
- this._mouseWheelToDispose = [];
- this._setListeningToMouseWheel(this._options.handleMouseWheel);
- this.onmouseover(this._listenOnDomNode, (e) => this._onMouseOver(e));
- this.onmouseleave(this._listenOnDomNode, (e) => this._onMouseLeave(e));
- this._hideTimeout = this._register(new TimeoutTimer());
- this._isDragging = false;
- this._mouseIsOver = false;
- this._shouldRender = true;
- this._revealOnScroll = true;
- }
- get options() {
- return this._options;
- }
- dispose() {
- this._mouseWheelToDispose = dispose(this._mouseWheelToDispose);
- super.dispose();
- }
- /**
- * Get the generated 'scrollable' dom node
- */
- getDomNode() {
- return this._domNode;
- }
- getOverviewRulerLayoutInfo() {
- return {
- parent: this._domNode,
- insertBefore: this._verticalScrollbar.domNode.domNode,
- };
- }
- /**
- * Delegate a pointer down event to the vertical scrollbar.
- * This is to help with clicking somewhere else and having the scrollbar react.
- */
- delegateVerticalScrollbarPointerDown(browserEvent) {
- this._verticalScrollbar.delegatePointerDown(browserEvent);
- }
- getScrollDimensions() {
- return this._scrollable.getScrollDimensions();
- }
- setScrollDimensions(dimensions) {
- this._scrollable.setScrollDimensions(dimensions, false);
- }
- /**
- * Update the class name of the scrollable element.
- */
- updateClassName(newClassName) {
- this._options.className = newClassName;
- // Defaults are different on Macs
- if (platform.isMacintosh) {
- this._options.className += ' mac';
- }
- this._domNode.className = 'monaco-scrollable-element ' + this._options.className;
- }
- /**
- * Update configuration options for the scrollbar.
- */
- updateOptions(newOptions) {
- if (typeof newOptions.handleMouseWheel !== 'undefined') {
- this._options.handleMouseWheel = newOptions.handleMouseWheel;
- this._setListeningToMouseWheel(this._options.handleMouseWheel);
- }
- if (typeof newOptions.mouseWheelScrollSensitivity !== 'undefined') {
- this._options.mouseWheelScrollSensitivity = newOptions.mouseWheelScrollSensitivity;
- }
- if (typeof newOptions.fastScrollSensitivity !== 'undefined') {
- this._options.fastScrollSensitivity = newOptions.fastScrollSensitivity;
- }
- if (typeof newOptions.scrollPredominantAxis !== 'undefined') {
- this._options.scrollPredominantAxis = newOptions.scrollPredominantAxis;
- }
- if (typeof newOptions.horizontal !== 'undefined') {
- this._options.horizontal = newOptions.horizontal;
- }
- if (typeof newOptions.vertical !== 'undefined') {
- this._options.vertical = newOptions.vertical;
- }
- if (typeof newOptions.horizontalScrollbarSize !== 'undefined') {
- this._options.horizontalScrollbarSize = newOptions.horizontalScrollbarSize;
- }
- if (typeof newOptions.verticalScrollbarSize !== 'undefined') {
- this._options.verticalScrollbarSize = newOptions.verticalScrollbarSize;
- }
- if (typeof newOptions.scrollByPage !== 'undefined') {
- this._options.scrollByPage = newOptions.scrollByPage;
- }
- this._horizontalScrollbar.updateOptions(this._options);
- this._verticalScrollbar.updateOptions(this._options);
- if (!this._options.lazyRender) {
- this._render();
- }
- }
- // -------------------- mouse wheel scrolling --------------------
- _setListeningToMouseWheel(shouldListen) {
- const isListening = (this._mouseWheelToDispose.length > 0);
- if (isListening === shouldListen) {
- // No change
- return;
- }
- // Stop listening (if necessary)
- this._mouseWheelToDispose = dispose(this._mouseWheelToDispose);
- // Start listening (if necessary)
- if (shouldListen) {
- const onMouseWheel = (browserEvent) => {
- this._onMouseWheel(new StandardWheelEvent(browserEvent));
- };
- this._mouseWheelToDispose.push(dom.addDisposableListener(this._listenOnDomNode, dom.EventType.MOUSE_WHEEL, onMouseWheel, { passive: false }));
- }
- }
- _onMouseWheel(e) {
- const classifier = MouseWheelClassifier.INSTANCE;
- if (SCROLL_WHEEL_SMOOTH_SCROLL_ENABLED) {
- const osZoomFactor = window.devicePixelRatio / getZoomFactor();
- if (platform.isWindows || platform.isLinux) {
- // On Windows and Linux, the incoming delta events are multiplied with the OS zoom factor.
- // The OS zoom factor can be reverse engineered by using the device pixel ratio and the configured zoom factor into account.
- classifier.accept(Date.now(), e.deltaX / osZoomFactor, e.deltaY / osZoomFactor);
- }
- else {
- classifier.accept(Date.now(), e.deltaX, e.deltaY);
- }
- }
- // console.log(`${Date.now()}, ${e.deltaY}, ${e.deltaX}`);
- let didScroll = false;
- if (e.deltaY || e.deltaX) {
- let deltaY = e.deltaY * this._options.mouseWheelScrollSensitivity;
- let deltaX = e.deltaX * this._options.mouseWheelScrollSensitivity;
- if (this._options.scrollPredominantAxis) {
- if (Math.abs(deltaY) >= Math.abs(deltaX)) {
- deltaX = 0;
- }
- else {
- deltaY = 0;
- }
- }
- if (this._options.flipAxes) {
- [deltaY, deltaX] = [deltaX, deltaY];
- }
- // Convert vertical scrolling to horizontal if shift is held, this
- // is handled at a higher level on Mac
- const shiftConvert = !platform.isMacintosh && e.browserEvent && e.browserEvent.shiftKey;
- if ((this._options.scrollYToX || shiftConvert) && !deltaX) {
- deltaX = deltaY;
- deltaY = 0;
- }
- if (e.browserEvent && e.browserEvent.altKey) {
- // fastScrolling
- deltaX = deltaX * this._options.fastScrollSensitivity;
- deltaY = deltaY * this._options.fastScrollSensitivity;
- }
- const futureScrollPosition = this._scrollable.getFutureScrollPosition();
- let desiredScrollPosition = {};
- if (deltaY) {
- const deltaScrollTop = SCROLL_WHEEL_SENSITIVITY * deltaY;
- // Here we convert values such as -0.3 to -1 or 0.3 to 1, otherwise low speed scrolling will never scroll
- const desiredScrollTop = futureScrollPosition.scrollTop - (deltaScrollTop < 0 ? Math.floor(deltaScrollTop) : Math.ceil(deltaScrollTop));
- this._verticalScrollbar.writeScrollPosition(desiredScrollPosition, desiredScrollTop);
- }
- if (deltaX) {
- const deltaScrollLeft = SCROLL_WHEEL_SENSITIVITY * deltaX;
- // Here we convert values such as -0.3 to -1 or 0.3 to 1, otherwise low speed scrolling will never scroll
- const desiredScrollLeft = futureScrollPosition.scrollLeft - (deltaScrollLeft < 0 ? Math.floor(deltaScrollLeft) : Math.ceil(deltaScrollLeft));
- this._horizontalScrollbar.writeScrollPosition(desiredScrollPosition, desiredScrollLeft);
- }
- // Check that we are scrolling towards a location which is valid
- desiredScrollPosition = this._scrollable.validateScrollPosition(desiredScrollPosition);
- if (futureScrollPosition.scrollLeft !== desiredScrollPosition.scrollLeft || futureScrollPosition.scrollTop !== desiredScrollPosition.scrollTop) {
- const canPerformSmoothScroll = (SCROLL_WHEEL_SMOOTH_SCROLL_ENABLED
- && this._options.mouseWheelSmoothScroll
- && classifier.isPhysicalMouseWheel());
- if (canPerformSmoothScroll) {
- this._scrollable.setScrollPositionSmooth(desiredScrollPosition);
- }
- else {
- this._scrollable.setScrollPositionNow(desiredScrollPosition);
- }
- didScroll = true;
- }
- }
- let consumeMouseWheel = didScroll;
- if (!consumeMouseWheel && this._options.alwaysConsumeMouseWheel) {
- consumeMouseWheel = true;
- }
- if (!consumeMouseWheel && this._options.consumeMouseWheelIfScrollbarIsNeeded && (this._verticalScrollbar.isNeeded() || this._horizontalScrollbar.isNeeded())) {
- consumeMouseWheel = true;
- }
- if (consumeMouseWheel) {
- e.preventDefault();
- e.stopPropagation();
- }
- }
- _onDidScroll(e) {
- this._shouldRender = this._horizontalScrollbar.onDidScroll(e) || this._shouldRender;
- this._shouldRender = this._verticalScrollbar.onDidScroll(e) || this._shouldRender;
- if (this._options.useShadows) {
- this._shouldRender = true;
- }
- if (this._revealOnScroll) {
- this._reveal();
- }
- if (!this._options.lazyRender) {
- this._render();
- }
- }
- /**
- * Render / mutate the DOM now.
- * Should be used together with the ctor option `lazyRender`.
- */
- renderNow() {
- if (!this._options.lazyRender) {
- throw new Error('Please use `lazyRender` together with `renderNow`!');
- }
- this._render();
- }
- _render() {
- if (!this._shouldRender) {
- return;
- }
- this._shouldRender = false;
- this._horizontalScrollbar.render();
- this._verticalScrollbar.render();
- if (this._options.useShadows) {
- const scrollState = this._scrollable.getCurrentScrollPosition();
- const enableTop = scrollState.scrollTop > 0;
- const enableLeft = scrollState.scrollLeft > 0;
- const leftClassName = (enableLeft ? ' left' : '');
- const topClassName = (enableTop ? ' top' : '');
- const topLeftClassName = (enableLeft || enableTop ? ' top-left-corner' : '');
- this._leftShadowDomNode.setClassName(`shadow${leftClassName}`);
- this._topShadowDomNode.setClassName(`shadow${topClassName}`);
- this._topLeftShadowDomNode.setClassName(`shadow${topLeftClassName}${topClassName}${leftClassName}`);
- }
- }
- // -------------------- fade in / fade out --------------------
- _onDragStart() {
- this._isDragging = true;
- this._reveal();
- }
- _onDragEnd() {
- this._isDragging = false;
- this._hide();
- }
- _onMouseLeave(e) {
- this._mouseIsOver = false;
- this._hide();
- }
- _onMouseOver(e) {
- this._mouseIsOver = true;
- this._reveal();
- }
- _reveal() {
- this._verticalScrollbar.beginReveal();
- this._horizontalScrollbar.beginReveal();
- this._scheduleHide();
- }
- _hide() {
- if (!this._mouseIsOver && !this._isDragging) {
- this._verticalScrollbar.beginHide();
- this._horizontalScrollbar.beginHide();
- }
- }
- _scheduleHide() {
- if (!this._mouseIsOver && !this._isDragging) {
- this._hideTimeout.cancelAndSet(() => this._hide(), HIDE_TIMEOUT);
- }
- }
- }
- export class ScrollableElement extends AbstractScrollableElement {
- constructor(element, options) {
- options = options || {};
- options.mouseWheelSmoothScroll = false;
- const scrollable = new Scrollable({
- forceIntegerValues: true,
- smoothScrollDuration: 0,
- scheduleAtNextAnimationFrame: (callback) => dom.scheduleAtNextAnimationFrame(callback)
- });
- super(element, options, scrollable);
- this._register(scrollable);
- }
- setScrollPosition(update) {
- this._scrollable.setScrollPositionNow(update);
- }
- }
- export class SmoothScrollableElement extends AbstractScrollableElement {
- constructor(element, options, scrollable) {
- super(element, options, scrollable);
- }
- setScrollPosition(update) {
- if (update.reuseAnimation) {
- this._scrollable.setScrollPositionSmooth(update, update.reuseAnimation);
- }
- else {
- this._scrollable.setScrollPositionNow(update);
- }
- }
- getScrollPosition() {
- return this._scrollable.getCurrentScrollPosition();
- }
- }
- export class DomScrollableElement extends AbstractScrollableElement {
- constructor(element, options) {
- options = options || {};
- options.mouseWheelSmoothScroll = false;
- const scrollable = new Scrollable({
- forceIntegerValues: false,
- smoothScrollDuration: 0,
- scheduleAtNextAnimationFrame: (callback) => dom.scheduleAtNextAnimationFrame(callback)
- });
- super(element, options, scrollable);
- this._register(scrollable);
- this._element = element;
- this.onScroll((e) => {
- if (e.scrollTopChanged) {
- this._element.scrollTop = e.scrollTop;
- }
- if (e.scrollLeftChanged) {
- this._element.scrollLeft = e.scrollLeft;
- }
- });
- this.scanDomNode();
- }
- setScrollPosition(update) {
- this._scrollable.setScrollPositionNow(update);
- }
- getScrollPosition() {
- return this._scrollable.getCurrentScrollPosition();
- }
- scanDomNode() {
- // width, scrollLeft, scrollWidth, height, scrollTop, scrollHeight
- this.setScrollDimensions({
- width: this._element.clientWidth,
- scrollWidth: this._element.scrollWidth,
- height: this._element.clientHeight,
- scrollHeight: this._element.scrollHeight
- });
- this.setScrollPosition({
- scrollLeft: this._element.scrollLeft,
- scrollTop: this._element.scrollTop,
- });
- }
- }
- function resolveOptions(opts) {
- const result = {
- lazyRender: (typeof opts.lazyRender !== 'undefined' ? opts.lazyRender : false),
- className: (typeof opts.className !== 'undefined' ? opts.className : ''),
- useShadows: (typeof opts.useShadows !== 'undefined' ? opts.useShadows : true),
- handleMouseWheel: (typeof opts.handleMouseWheel !== 'undefined' ? opts.handleMouseWheel : true),
- flipAxes: (typeof opts.flipAxes !== 'undefined' ? opts.flipAxes : false),
- consumeMouseWheelIfScrollbarIsNeeded: (typeof opts.consumeMouseWheelIfScrollbarIsNeeded !== 'undefined' ? opts.consumeMouseWheelIfScrollbarIsNeeded : false),
- alwaysConsumeMouseWheel: (typeof opts.alwaysConsumeMouseWheel !== 'undefined' ? opts.alwaysConsumeMouseWheel : false),
- scrollYToX: (typeof opts.scrollYToX !== 'undefined' ? opts.scrollYToX : false),
- mouseWheelScrollSensitivity: (typeof opts.mouseWheelScrollSensitivity !== 'undefined' ? opts.mouseWheelScrollSensitivity : 1),
- fastScrollSensitivity: (typeof opts.fastScrollSensitivity !== 'undefined' ? opts.fastScrollSensitivity : 5),
- scrollPredominantAxis: (typeof opts.scrollPredominantAxis !== 'undefined' ? opts.scrollPredominantAxis : true),
- mouseWheelSmoothScroll: (typeof opts.mouseWheelSmoothScroll !== 'undefined' ? opts.mouseWheelSmoothScroll : true),
- arrowSize: (typeof opts.arrowSize !== 'undefined' ? opts.arrowSize : 11),
- listenOnDomNode: (typeof opts.listenOnDomNode !== 'undefined' ? opts.listenOnDomNode : null),
- horizontal: (typeof opts.horizontal !== 'undefined' ? opts.horizontal : 1 /* ScrollbarVisibility.Auto */),
- horizontalScrollbarSize: (typeof opts.horizontalScrollbarSize !== 'undefined' ? opts.horizontalScrollbarSize : 10),
- horizontalSliderSize: (typeof opts.horizontalSliderSize !== 'undefined' ? opts.horizontalSliderSize : 0),
- horizontalHasArrows: (typeof opts.horizontalHasArrows !== 'undefined' ? opts.horizontalHasArrows : false),
- vertical: (typeof opts.vertical !== 'undefined' ? opts.vertical : 1 /* ScrollbarVisibility.Auto */),
- verticalScrollbarSize: (typeof opts.verticalScrollbarSize !== 'undefined' ? opts.verticalScrollbarSize : 10),
- verticalHasArrows: (typeof opts.verticalHasArrows !== 'undefined' ? opts.verticalHasArrows : false),
- verticalSliderSize: (typeof opts.verticalSliderSize !== 'undefined' ? opts.verticalSliderSize : 0),
- scrollByPage: (typeof opts.scrollByPage !== 'undefined' ? opts.scrollByPage : false)
- };
- result.horizontalSliderSize = (typeof opts.horizontalSliderSize !== 'undefined' ? opts.horizontalSliderSize : result.horizontalScrollbarSize);
- result.verticalSliderSize = (typeof opts.verticalSliderSize !== 'undefined' ? opts.verticalSliderSize : result.verticalScrollbarSize);
- // Defaults are different on Macs
- if (platform.isMacintosh) {
- result.className += ' mac';
- }
- return result;
- }
|