xterm
[VSoRC/.git] / node_modules / xterm / src / browser / Viewport.ts
1 /**
2  * Copyright (c) 2016 The xterm.js authors. All rights reserved.
3  * @license MIT
4  */
5
6 import { Disposable } from 'common/Lifecycle';
7 import { addDisposableDomListener } from 'browser/Lifecycle';
8 import { IColorSet, IViewport } from 'browser/Types';
9 import { ICharSizeService, IRenderService } from 'browser/services/Services';
10 import { IBufferService, IOptionsService } from 'common/services/Services';
11
12 const FALLBACK_SCROLL_BAR_WIDTH = 15;
13
14 /**
15  * Represents the viewport of a terminal, the visible area within the larger buffer of output.
16  * Logic for the virtual scroll bar is included in this object.
17  */
18 export class Viewport extends Disposable implements IViewport {
19   public scrollBarWidth: number = 0;
20   private _currentRowHeight: number = 0;
21   private _lastRecordedBufferLength: number = 0;
22   private _lastRecordedViewportHeight: number = 0;
23   private _lastRecordedBufferHeight: number = 0;
24   private _lastTouchY: number = 0;
25   private _lastScrollTop: number = 0;
26
27   // Stores a partial line amount when scrolling, this is used to keep track of how much of a line
28   // is scrolled so we can "scroll" over partial lines and feel natural on touchpads. This is a
29   // quick fix and could have a more robust solution in place that reset the value when needed.
30   private _wheelPartialScroll: number = 0;
31
32   private _refreshAnimationFrame: number | null = null;
33   private _ignoreNextScrollEvent: boolean = false;
34
35   constructor(
36     private readonly _scrollLines: (amount: number, suppressEvent: boolean) => void,
37     private readonly _viewportElement: HTMLElement,
38     private readonly _scrollArea: HTMLElement,
39     @IBufferService private readonly _bufferService: IBufferService,
40     @IOptionsService private readonly _optionsService: IOptionsService,
41     @ICharSizeService private readonly _charSizeService: ICharSizeService,
42     @IRenderService private readonly _renderService: IRenderService
43   ) {
44     super();
45
46     // Measure the width of the scrollbar. If it is 0 we can assume it's an OSX overlay scrollbar.
47     // Unfortunately the overlay scrollbar would be hidden underneath the screen element in that case,
48     // therefore we account for a standard amount to make it visible
49     this.scrollBarWidth = (this._viewportElement.offsetWidth - this._scrollArea.offsetWidth) || FALLBACK_SCROLL_BAR_WIDTH;
50     this.register(addDisposableDomListener(this._viewportElement, 'scroll', this._onScroll.bind(this)));
51
52     // Perform this async to ensure the ICharSizeService is ready.
53     setTimeout(() => this.syncScrollArea(), 0);
54   }
55
56   public onThemeChange(colors: IColorSet): void {
57     this._viewportElement.style.backgroundColor = colors.background.css;
58   }
59
60   /**
61    * Refreshes row height, setting line-height, viewport height and scroll area height if
62    * necessary.
63    */
64   private _refresh(immediate: boolean): void {
65     if (immediate) {
66       this._innerRefresh();
67       if (this._refreshAnimationFrame !== null) {
68         cancelAnimationFrame(this._refreshAnimationFrame);
69       }
70       return;
71     }
72     if (this._refreshAnimationFrame === null) {
73       this._refreshAnimationFrame = requestAnimationFrame(() => this._innerRefresh());
74     }
75   }
76
77   private _innerRefresh(): void {
78     if (this._charSizeService.height > 0) {
79       this._currentRowHeight = this._renderService.dimensions.scaledCellHeight / window.devicePixelRatio;
80       this._lastRecordedViewportHeight = this._viewportElement.offsetHeight;
81       const newBufferHeight = Math.round(this._currentRowHeight * this._lastRecordedBufferLength) + (this._lastRecordedViewportHeight - this._renderService.dimensions.canvasHeight);
82       if (this._lastRecordedBufferHeight !== newBufferHeight) {
83         this._lastRecordedBufferHeight = newBufferHeight;
84         this._scrollArea.style.height = this._lastRecordedBufferHeight + 'px';
85       }
86     }
87
88     // Sync scrollTop
89     const scrollTop = this._bufferService.buffer.ydisp * this._currentRowHeight;
90     if (this._viewportElement.scrollTop !== scrollTop) {
91       // Ignore the next scroll event which will be triggered by setting the scrollTop as we do not
92       // want this event to scroll the terminal
93       this._ignoreNextScrollEvent = true;
94       this._viewportElement.scrollTop = scrollTop;
95     }
96
97     this._refreshAnimationFrame = null;
98   }
99   /**
100    * Updates dimensions and synchronizes the scroll area if necessary.
101    */
102   public syncScrollArea(immediate: boolean = false): void {
103     // If buffer height changed
104     if (this._lastRecordedBufferLength !== this._bufferService.buffer.lines.length) {
105       this._lastRecordedBufferLength = this._bufferService.buffer.lines.length;
106       this._refresh(immediate);
107       return;
108     }
109
110     // If viewport height changed
111     if (this._lastRecordedViewportHeight !== this._renderService.dimensions.canvasHeight) {
112       this._refresh(immediate);
113       return;
114     }
115
116     // If the buffer position doesn't match last scroll top
117     const newScrollTop = this._bufferService.buffer.ydisp * this._currentRowHeight;
118     if (this._lastScrollTop !== newScrollTop) {
119       this._refresh(immediate);
120       return;
121     }
122
123     // If element's scroll top changed, this can happen when hiding the element
124     if (this._lastScrollTop !== this._viewportElement.scrollTop) {
125       this._refresh(immediate);
126       return;
127     }
128
129     // If row height changed
130     if (this._renderService.dimensions.scaledCellHeight / window.devicePixelRatio !== this._currentRowHeight) {
131       this._refresh(immediate);
132       return;
133     }
134   }
135
136   /**
137    * Handles scroll events on the viewport, calculating the new viewport and requesting the
138    * terminal to scroll to it.
139    * @param ev The scroll event.
140    */
141   private _onScroll(ev: Event): void {
142     // Record current scroll top position
143     this._lastScrollTop = this._viewportElement.scrollTop;
144
145     // Don't attempt to scroll if the element is not visible, otherwise scrollTop will be corrupt
146     // which causes the terminal to scroll the buffer to the top
147     if (!this._viewportElement.offsetParent) {
148       return;
149     }
150
151     // Ignore the event if it was flagged to ignore (when the source of the event is from Viewport)
152     if (this._ignoreNextScrollEvent) {
153       this._ignoreNextScrollEvent = false;
154       return;
155     }
156
157     const newRow = Math.round(this._lastScrollTop / this._currentRowHeight);
158     const diff = newRow - this._bufferService.buffer.ydisp;
159     this._scrollLines(diff, true);
160   }
161
162   /**
163    * Handles bubbling of scroll event in case the viewport has reached top or bottom
164    * @param ev The scroll event.
165    * @param amount The amount scrolled
166    */
167   private _bubbleScroll(ev: Event, amount: number): boolean {
168     const scrollPosFromTop = this._viewportElement.scrollTop + this._lastRecordedViewportHeight;
169     if ((amount < 0 && this._viewportElement.scrollTop !== 0) ||
170         (amount > 0 &&  scrollPosFromTop < this._lastRecordedBufferHeight)) {
171       if (ev.cancelable) {
172         ev.preventDefault();
173       }
174       return false;
175     }
176     return true;
177   }
178
179   /**
180    * Handles mouse wheel events by adjusting the viewport's scrollTop and delegating the actual
181    * scrolling to `onScroll`, this event needs to be attached manually by the consumer of
182    * `Viewport`.
183    * @param ev The mouse wheel event.
184    */
185   public onWheel(ev: WheelEvent): boolean {
186     const amount = this._getPixelsScrolled(ev);
187     if (amount === 0) {
188       return false;
189     }
190     this._viewportElement.scrollTop += amount;
191     return this._bubbleScroll(ev, amount);
192   }
193
194   private _getPixelsScrolled(ev: WheelEvent): number {
195     // Do nothing if it's not a vertical scroll event
196     if (ev.deltaY === 0) {
197       return 0;
198     }
199
200     // Fallback to WheelEvent.DOM_DELTA_PIXEL
201     let amount = this._applyScrollModifier(ev.deltaY, ev);
202     if (ev.deltaMode === WheelEvent.DOM_DELTA_LINE) {
203       amount *= this._currentRowHeight;
204     } else if (ev.deltaMode === WheelEvent.DOM_DELTA_PAGE) {
205       amount *= this._currentRowHeight * this._bufferService.rows;
206     }
207     return amount;
208   }
209
210   /**
211    * Gets the number of pixels scrolled by the mouse event taking into account what type of delta
212    * is being used.
213    * @param ev The mouse wheel event.
214    */
215   public getLinesScrolled(ev: WheelEvent): number {
216     // Do nothing if it's not a vertical scroll event
217     if (ev.deltaY === 0) {
218       return 0;
219     }
220
221     // Fallback to WheelEvent.DOM_DELTA_LINE
222     let amount = this._applyScrollModifier(ev.deltaY, ev);
223     if (ev.deltaMode === WheelEvent.DOM_DELTA_PIXEL) {
224       amount /= this._currentRowHeight + 0.0; // Prevent integer division
225       this._wheelPartialScroll += amount;
226       amount = Math.floor(Math.abs(this._wheelPartialScroll)) * (this._wheelPartialScroll > 0 ? 1 : -1);
227       this._wheelPartialScroll %= 1;
228     } else if (ev.deltaMode === WheelEvent.DOM_DELTA_PAGE) {
229       amount *= this._bufferService.rows;
230     }
231     return amount;
232   }
233
234   private _applyScrollModifier(amount: number, ev: WheelEvent): number {
235     const modifier = this._optionsService.options.fastScrollModifier;
236     // Multiply the scroll speed when the modifier is down
237     if ((modifier === 'alt' && ev.altKey) ||
238         (modifier === 'ctrl' && ev.ctrlKey) ||
239         (modifier === 'shift' && ev.shiftKey)) {
240       return amount * this._optionsService.options.fastScrollSensitivity * this._optionsService.options.scrollSensitivity;
241     }
242
243     return amount * this._optionsService.options.scrollSensitivity;
244   }
245
246   /**
247    * Handles the touchstart event, recording the touch occurred.
248    * @param ev The touch event.
249    */
250   public onTouchStart(ev: TouchEvent): void {
251     this._lastTouchY = ev.touches[0].pageY;
252   }
253
254   /**
255    * Handles the touchmove event, scrolling the viewport if the position shifted.
256    * @param ev The touch event.
257    */
258   public onTouchMove(ev: TouchEvent): boolean {
259     const deltaY = this._lastTouchY - ev.touches[0].pageY;
260     this._lastTouchY = ev.touches[0].pageY;
261     if (deltaY === 0) {
262       return false;
263     }
264     this._viewportElement.scrollTop += deltaY;
265     return this._bubbleScroll(ev, deltaY);
266   }
267 }