Path: blob/main/src/vs/editor/common/viewLayout/viewLayout.ts
3294 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { Event, Emitter } from '../../../base/common/event.js';6import { Disposable, IDisposable } from '../../../base/common/lifecycle.js';7import { IScrollPosition, ScrollEvent, Scrollable, ScrollbarVisibility, INewScrollPosition } from '../../../base/common/scrollable.js';8import { ConfigurationChangedEvent, EditorOption } from '../config/editorOptions.js';9import { ScrollType } from '../editorCommon.js';10import { IEditorConfiguration } from '../config/editorConfiguration.js';11import { LinesLayout } from './linesLayout.js';12import { IEditorWhitespace, IPartialViewLinesViewportData, ILineHeightChangeAccessor, IViewLayout, IViewWhitespaceViewportData, IWhitespaceChangeAccessor, Viewport } from '../viewModel.js';13import { ContentSizeChangedEvent } from '../viewModelEventDispatcher.js';14import { ICustomLineHeightData } from './lineHeights.js';1516const SMOOTH_SCROLLING_TIME = 125;1718class EditorScrollDimensions {1920public readonly width: number;21public readonly contentWidth: number;22public readonly scrollWidth: number;2324public readonly height: number;25public readonly contentHeight: number;26public readonly scrollHeight: number;2728constructor(29width: number,30contentWidth: number,31height: number,32contentHeight: number,33) {34width = width | 0;35contentWidth = contentWidth | 0;36height = height | 0;37contentHeight = contentHeight | 0;3839if (width < 0) {40width = 0;41}42if (contentWidth < 0) {43contentWidth = 0;44}4546if (height < 0) {47height = 0;48}49if (contentHeight < 0) {50contentHeight = 0;51}5253this.width = width;54this.contentWidth = contentWidth;55this.scrollWidth = Math.max(width, contentWidth);5657this.height = height;58this.contentHeight = contentHeight;59this.scrollHeight = Math.max(height, contentHeight);60}6162public equals(other: EditorScrollDimensions): boolean {63return (64this.width === other.width65&& this.contentWidth === other.contentWidth66&& this.height === other.height67&& this.contentHeight === other.contentHeight68);69}70}7172class EditorScrollable extends Disposable {7374private readonly _scrollable: Scrollable;75private _dimensions: EditorScrollDimensions;7677public readonly onDidScroll: Event<ScrollEvent>;7879private readonly _onDidContentSizeChange = this._register(new Emitter<ContentSizeChangedEvent>());80public readonly onDidContentSizeChange: Event<ContentSizeChangedEvent> = this._onDidContentSizeChange.event;8182constructor(smoothScrollDuration: number, scheduleAtNextAnimationFrame: (callback: () => void) => IDisposable) {83super();84this._dimensions = new EditorScrollDimensions(0, 0, 0, 0);85this._scrollable = this._register(new Scrollable({86forceIntegerValues: true,87smoothScrollDuration,88scheduleAtNextAnimationFrame89}));90this.onDidScroll = this._scrollable.onScroll;91}9293public getScrollable(): Scrollable {94return this._scrollable;95}9697public setSmoothScrollDuration(smoothScrollDuration: number): void {98this._scrollable.setSmoothScrollDuration(smoothScrollDuration);99}100101public validateScrollPosition(scrollPosition: INewScrollPosition): IScrollPosition {102return this._scrollable.validateScrollPosition(scrollPosition);103}104105public getScrollDimensions(): EditorScrollDimensions {106return this._dimensions;107}108109public setScrollDimensions(dimensions: EditorScrollDimensions): void {110if (this._dimensions.equals(dimensions)) {111return;112}113114const oldDimensions = this._dimensions;115this._dimensions = dimensions;116117this._scrollable.setScrollDimensions({118width: dimensions.width,119scrollWidth: dimensions.scrollWidth,120height: dimensions.height,121scrollHeight: dimensions.scrollHeight122}, true);123124const contentWidthChanged = (oldDimensions.contentWidth !== dimensions.contentWidth);125const contentHeightChanged = (oldDimensions.contentHeight !== dimensions.contentHeight);126if (contentWidthChanged || contentHeightChanged) {127this._onDidContentSizeChange.fire(new ContentSizeChangedEvent(128oldDimensions.contentWidth, oldDimensions.contentHeight,129dimensions.contentWidth, dimensions.contentHeight130));131}132}133134public getFutureScrollPosition(): IScrollPosition {135return this._scrollable.getFutureScrollPosition();136}137138public getCurrentScrollPosition(): IScrollPosition {139return this._scrollable.getCurrentScrollPosition();140}141142public setScrollPositionNow(update: INewScrollPosition): void {143this._scrollable.setScrollPositionNow(update);144}145146public setScrollPositionSmooth(update: INewScrollPosition): void {147this._scrollable.setScrollPositionSmooth(update);148}149150public hasPendingScrollAnimation(): boolean {151return this._scrollable.hasPendingScrollAnimation();152}153}154155export class ViewLayout extends Disposable implements IViewLayout {156157private readonly _configuration: IEditorConfiguration;158private readonly _linesLayout: LinesLayout;159private _maxLineWidth: number;160private _overlayWidgetsMinWidth: number;161162private readonly _scrollable: EditorScrollable;163public readonly onDidScroll: Event<ScrollEvent>;164public readonly onDidContentSizeChange: Event<ContentSizeChangedEvent>;165166constructor(configuration: IEditorConfiguration, lineCount: number, customLineHeightData: ICustomLineHeightData[], scheduleAtNextAnimationFrame: (callback: () => void) => IDisposable) {167super();168169this._configuration = configuration;170const options = this._configuration.options;171const layoutInfo = options.get(EditorOption.layoutInfo);172const padding = options.get(EditorOption.padding);173174this._linesLayout = new LinesLayout(lineCount, options.get(EditorOption.lineHeight), padding.top, padding.bottom, customLineHeightData);175this._maxLineWidth = 0;176this._overlayWidgetsMinWidth = 0;177178this._scrollable = this._register(new EditorScrollable(0, scheduleAtNextAnimationFrame));179this._configureSmoothScrollDuration();180181this._scrollable.setScrollDimensions(new EditorScrollDimensions(182layoutInfo.contentWidth,1830,184layoutInfo.height,1850186));187this.onDidScroll = this._scrollable.onDidScroll;188this.onDidContentSizeChange = this._scrollable.onDidContentSizeChange;189190this._updateHeight();191}192193public override dispose(): void {194super.dispose();195}196197public getScrollable(): Scrollable {198return this._scrollable.getScrollable();199}200201public onHeightMaybeChanged(): void {202this._updateHeight();203}204205private _configureSmoothScrollDuration(): void {206this._scrollable.setSmoothScrollDuration(this._configuration.options.get(EditorOption.smoothScrolling) ? SMOOTH_SCROLLING_TIME : 0);207}208209// ---- begin view event handlers210211public onConfigurationChanged(e: ConfigurationChangedEvent): void {212const options = this._configuration.options;213if (e.hasChanged(EditorOption.lineHeight)) {214this._linesLayout.setDefaultLineHeight(options.get(EditorOption.lineHeight));215}216if (e.hasChanged(EditorOption.padding)) {217const padding = options.get(EditorOption.padding);218this._linesLayout.setPadding(padding.top, padding.bottom);219}220if (e.hasChanged(EditorOption.layoutInfo)) {221const layoutInfo = options.get(EditorOption.layoutInfo);222const width = layoutInfo.contentWidth;223const height = layoutInfo.height;224const scrollDimensions = this._scrollable.getScrollDimensions();225const contentWidth = scrollDimensions.contentWidth;226this._scrollable.setScrollDimensions(new EditorScrollDimensions(227width,228scrollDimensions.contentWidth,229height,230this._getContentHeight(width, height, contentWidth)231));232} else {233this._updateHeight();234}235if (e.hasChanged(EditorOption.smoothScrolling)) {236this._configureSmoothScrollDuration();237}238}239public onFlushed(lineCount: number, customLineHeightData: ICustomLineHeightData[]): void {240this._linesLayout.onFlushed(lineCount, customLineHeightData);241}242public onLinesDeleted(fromLineNumber: number, toLineNumber: number): void {243this._linesLayout.onLinesDeleted(fromLineNumber, toLineNumber);244}245public onLinesInserted(fromLineNumber: number, toLineNumber: number): void {246this._linesLayout.onLinesInserted(fromLineNumber, toLineNumber);247}248249// ---- end view event handlers250251private _getHorizontalScrollbarHeight(width: number, scrollWidth: number): number {252const options = this._configuration.options;253const scrollbar = options.get(EditorOption.scrollbar);254if (scrollbar.horizontal === ScrollbarVisibility.Hidden) {255// horizontal scrollbar not visible256return 0;257}258if (width >= scrollWidth) {259// horizontal scrollbar not visible260return 0;261}262return scrollbar.horizontalScrollbarSize;263}264265private _getContentHeight(width: number, height: number, contentWidth: number): number {266const options = this._configuration.options;267268let result = this._linesLayout.getLinesTotalHeight();269if (options.get(EditorOption.scrollBeyondLastLine)) {270result += Math.max(0, height - options.get(EditorOption.lineHeight) - options.get(EditorOption.padding).bottom);271} else if (!options.get(EditorOption.scrollbar).ignoreHorizontalScrollbarInContentHeight) {272result += this._getHorizontalScrollbarHeight(width, contentWidth);273}274275return result;276}277278private _updateHeight(): void {279const scrollDimensions = this._scrollable.getScrollDimensions();280const width = scrollDimensions.width;281const height = scrollDimensions.height;282const contentWidth = scrollDimensions.contentWidth;283this._scrollable.setScrollDimensions(new EditorScrollDimensions(284width,285scrollDimensions.contentWidth,286height,287this._getContentHeight(width, height, contentWidth)288));289}290291// ---- Layouting logic292293public getCurrentViewport(): Viewport {294const scrollDimensions = this._scrollable.getScrollDimensions();295const currentScrollPosition = this._scrollable.getCurrentScrollPosition();296return new Viewport(297currentScrollPosition.scrollTop,298currentScrollPosition.scrollLeft,299scrollDimensions.width,300scrollDimensions.height301);302}303304public getFutureViewport(): Viewport {305const scrollDimensions = this._scrollable.getScrollDimensions();306const currentScrollPosition = this._scrollable.getFutureScrollPosition();307return new Viewport(308currentScrollPosition.scrollTop,309currentScrollPosition.scrollLeft,310scrollDimensions.width,311scrollDimensions.height312);313}314315private _computeContentWidth(): number {316const options = this._configuration.options;317const maxLineWidth = this._maxLineWidth;318const wrappingInfo = options.get(EditorOption.wrappingInfo);319const fontInfo = options.get(EditorOption.fontInfo);320const layoutInfo = options.get(EditorOption.layoutInfo);321if (wrappingInfo.isViewportWrapping) {322const minimap = options.get(EditorOption.minimap);323if (maxLineWidth > layoutInfo.contentWidth + fontInfo.typicalHalfwidthCharacterWidth) {324// This is a case where viewport wrapping is on, but the line extends above the viewport325if (minimap.enabled && minimap.side === 'right') {326// We need to accomodate the scrollbar width327return maxLineWidth + layoutInfo.verticalScrollbarWidth;328}329}330return maxLineWidth;331} else {332const extraHorizontalSpace = options.get(EditorOption.scrollBeyondLastColumn) * fontInfo.typicalHalfwidthCharacterWidth;333const whitespaceMinWidth = this._linesLayout.getWhitespaceMinWidth();334return Math.max(maxLineWidth + extraHorizontalSpace + layoutInfo.verticalScrollbarWidth, whitespaceMinWidth, this._overlayWidgetsMinWidth);335}336}337338public setMaxLineWidth(maxLineWidth: number): void {339this._maxLineWidth = maxLineWidth;340this._updateContentWidth();341}342343public setOverlayWidgetsMinWidth(maxMinWidth: number): void {344this._overlayWidgetsMinWidth = maxMinWidth;345this._updateContentWidth();346}347348private _updateContentWidth(): void {349const scrollDimensions = this._scrollable.getScrollDimensions();350this._scrollable.setScrollDimensions(new EditorScrollDimensions(351scrollDimensions.width,352this._computeContentWidth(),353scrollDimensions.height,354scrollDimensions.contentHeight355));356357// The height might depend on the fact that there is a horizontal scrollbar or not358this._updateHeight();359}360361// ---- view state362363public saveState(): { scrollTop: number; scrollTopWithoutViewZones: number; scrollLeft: number } {364const currentScrollPosition = this._scrollable.getFutureScrollPosition();365const scrollTop = currentScrollPosition.scrollTop;366const firstLineNumberInViewport = this._linesLayout.getLineNumberAtOrAfterVerticalOffset(scrollTop);367const whitespaceAboveFirstLine = this._linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(firstLineNumberInViewport);368return {369scrollTop: scrollTop,370scrollTopWithoutViewZones: scrollTop - whitespaceAboveFirstLine,371scrollLeft: currentScrollPosition.scrollLeft372};373}374375// ----376public changeWhitespace(callback: (accessor: IWhitespaceChangeAccessor) => void): boolean {377const hadAChange = this._linesLayout.changeWhitespace(callback);378if (hadAChange) {379this.onHeightMaybeChanged();380}381return hadAChange;382}383384public changeSpecialLineHeights(callback: (accessor: ILineHeightChangeAccessor) => void): boolean {385const hadAChange = this._linesLayout.changeLineHeights(callback);386if (hadAChange) {387this.onHeightMaybeChanged();388}389return hadAChange;390}391392public getVerticalOffsetForLineNumber(lineNumber: number, includeViewZones: boolean = false): number {393return this._linesLayout.getVerticalOffsetForLineNumber(lineNumber, includeViewZones);394}395public getVerticalOffsetAfterLineNumber(lineNumber: number, includeViewZones: boolean = false): number {396return this._linesLayout.getVerticalOffsetAfterLineNumber(lineNumber, includeViewZones);397}398public getLineHeightForLineNumber(lineNumber: number): number {399return this._linesLayout.getLineHeightForLineNumber(lineNumber);400}401public isAfterLines(verticalOffset: number): boolean {402return this._linesLayout.isAfterLines(verticalOffset);403}404public isInTopPadding(verticalOffset: number): boolean {405return this._linesLayout.isInTopPadding(verticalOffset);406}407public isInBottomPadding(verticalOffset: number): boolean {408return this._linesLayout.isInBottomPadding(verticalOffset);409}410411public getLineNumberAtVerticalOffset(verticalOffset: number): number {412return this._linesLayout.getLineNumberAtOrAfterVerticalOffset(verticalOffset);413}414415public getWhitespaceAtVerticalOffset(verticalOffset: number): IViewWhitespaceViewportData | null {416return this._linesLayout.getWhitespaceAtVerticalOffset(verticalOffset);417}418public getLinesViewportData(): IPartialViewLinesViewportData {419const visibleBox = this.getCurrentViewport();420return this._linesLayout.getLinesViewportData(visibleBox.top, visibleBox.top + visibleBox.height);421}422public getLinesViewportDataAtScrollTop(scrollTop: number): IPartialViewLinesViewportData {423// do some minimal validations on scrollTop424const scrollDimensions = this._scrollable.getScrollDimensions();425if (scrollTop + scrollDimensions.height > scrollDimensions.scrollHeight) {426scrollTop = scrollDimensions.scrollHeight - scrollDimensions.height;427}428if (scrollTop < 0) {429scrollTop = 0;430}431return this._linesLayout.getLinesViewportData(scrollTop, scrollTop + scrollDimensions.height);432}433public getWhitespaceViewportData(): IViewWhitespaceViewportData[] {434const visibleBox = this.getCurrentViewport();435return this._linesLayout.getWhitespaceViewportData(visibleBox.top, visibleBox.top + visibleBox.height);436}437public getWhitespaces(): IEditorWhitespace[] {438return this._linesLayout.getWhitespaces();439}440441// ----442443public getContentWidth(): number {444const scrollDimensions = this._scrollable.getScrollDimensions();445return scrollDimensions.contentWidth;446}447public getScrollWidth(): number {448const scrollDimensions = this._scrollable.getScrollDimensions();449return scrollDimensions.scrollWidth;450}451public getContentHeight(): number {452const scrollDimensions = this._scrollable.getScrollDimensions();453return scrollDimensions.contentHeight;454}455public getScrollHeight(): number {456const scrollDimensions = this._scrollable.getScrollDimensions();457return scrollDimensions.scrollHeight;458}459460public getCurrentScrollLeft(): number {461const currentScrollPosition = this._scrollable.getCurrentScrollPosition();462return currentScrollPosition.scrollLeft;463}464public getCurrentScrollTop(): number {465const currentScrollPosition = this._scrollable.getCurrentScrollPosition();466return currentScrollPosition.scrollTop;467}468469public validateScrollPosition(scrollPosition: INewScrollPosition): IScrollPosition {470return this._scrollable.validateScrollPosition(scrollPosition);471}472473public setScrollPosition(position: INewScrollPosition, type: ScrollType): void {474if (type === ScrollType.Immediate) {475this._scrollable.setScrollPositionNow(position);476} else {477this._scrollable.setScrollPositionSmooth(position);478}479}480481public hasPendingScrollAnimation(): boolean {482return this._scrollable.hasPendingScrollAnimation();483}484485public deltaScrollNow(deltaScrollLeft: number, deltaScrollTop: number): void {486const currentScrollPosition = this._scrollable.getCurrentScrollPosition();487this._scrollable.setScrollPositionNow({488scrollLeft: currentScrollPosition.scrollLeft + deltaScrollLeft,489scrollTop: currentScrollPosition.scrollTop + deltaScrollTop490});491}492}493494495