Path: blob/main/src/vs/base/browser/ui/centered/centeredViewLayout.ts
3296 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 { $, IDomNodePagePosition } from '../../dom.js';6import { IView, IViewSize } from '../grid/grid.js';7import { IBoundarySashes } from '../sash/sash.js';8import { DistributeSizing, ISplitViewStyles, IView as ISplitViewView, Orientation, SplitView } from '../splitview/splitview.js';9import { Color } from '../../../common/color.js';10import { Event } from '../../../common/event.js';11import { DisposableStore, IDisposable } from '../../../common/lifecycle.js';1213export interface CenteredViewState {14// width of the fixed centered layout15targetWidth: number;16// proportional size of left margin17leftMarginRatio: number;18// proportional size of right margin19rightMarginRatio: number;20}2122const defaultState: CenteredViewState = {23targetWidth: 900,24leftMarginRatio: 0.1909,25rightMarginRatio: 0.1909,26};2728const distributeSizing: DistributeSizing = { type: 'distribute' };2930function createEmptyView(background: Color | undefined): ISplitViewView<{ top: number; left: number }> {31const element = $('.centered-layout-margin');32element.style.height = '100%';33if (background) {34element.style.backgroundColor = background.toString();35}3637return {38element,39layout: () => undefined,40minimumSize: 60,41maximumSize: Number.POSITIVE_INFINITY,42onDidChange: Event.None43};44}4546function toSplitViewView(view: IView, getHeight: () => number): ISplitViewView<{ top: number; left: number }> {47return {48element: view.element,49get maximumSize() { return view.maximumWidth; },50get minimumSize() { return view.minimumWidth; },51onDidChange: Event.map(view.onDidChange, e => e && e.width),52layout: (size, offset, ctx) => view.layout(size, getHeight(), ctx?.top ?? 0, (ctx?.left ?? 0) + offset)53};54}5556export interface ICenteredViewStyles extends ISplitViewStyles {57background: Color;58}5960export class CenteredViewLayout implements IDisposable {6162private splitView?: SplitView<{ top: number; left: number }>;63private lastLayoutPosition: IDomNodePagePosition = { width: 0, height: 0, left: 0, top: 0 };64private style!: ICenteredViewStyles;65private didLayout = false;66private emptyViews: ISplitViewView<{ top: number; left: number }>[] | undefined;67private readonly splitViewDisposables = new DisposableStore();6869constructor(70private container: HTMLElement,71private view: IView,72public state: CenteredViewState = { ...defaultState },73private centeredLayoutFixedWidth: boolean = false74) {75this.container.appendChild(this.view.element);76// Make sure to hide the split view overflow like sashes #5289277this.container.style.overflow = 'hidden';78}7980get minimumWidth(): number { return this.splitView ? this.splitView.minimumSize : this.view.minimumWidth; }81get maximumWidth(): number { return this.splitView ? this.splitView.maximumSize : this.view.maximumWidth; }82get minimumHeight(): number { return this.view.minimumHeight; }83get maximumHeight(): number { return this.view.maximumHeight; }84get onDidChange(): Event<IViewSize | undefined> { return this.view.onDidChange; }8586private _boundarySashes: IBoundarySashes = {};87get boundarySashes(): IBoundarySashes { return this._boundarySashes; }88set boundarySashes(boundarySashes: IBoundarySashes) {89this._boundarySashes = boundarySashes;9091if (!this.splitView) {92return;93}9495this.splitView.orthogonalStartSash = boundarySashes.top;96this.splitView.orthogonalEndSash = boundarySashes.bottom;97}9899layout(width: number, height: number, top: number, left: number): void {100this.lastLayoutPosition = { width, height, top, left };101if (this.splitView) {102this.splitView.layout(width, this.lastLayoutPosition);103if (!this.didLayout || this.centeredLayoutFixedWidth) {104this.resizeSplitViews();105}106} else {107this.view.layout(width, height, top, left);108}109110this.didLayout = true;111}112113private resizeSplitViews(): void {114if (!this.splitView) {115return;116}117if (this.centeredLayoutFixedWidth) {118const centerViewWidth = Math.min(this.lastLayoutPosition.width, this.state.targetWidth);119const marginWidthFloat = (this.lastLayoutPosition.width - centerViewWidth) / 2;120this.splitView.resizeView(0, Math.floor(marginWidthFloat));121this.splitView.resizeView(1, centerViewWidth);122this.splitView.resizeView(2, Math.ceil(marginWidthFloat));123} else {124const leftMargin = this.state.leftMarginRatio * this.lastLayoutPosition.width;125const rightMargin = this.state.rightMarginRatio * this.lastLayoutPosition.width;126const center = this.lastLayoutPosition.width - leftMargin - rightMargin;127this.splitView.resizeView(0, leftMargin);128this.splitView.resizeView(1, center);129this.splitView.resizeView(2, rightMargin);130}131}132133setFixedWidth(option: boolean) {134this.centeredLayoutFixedWidth = option;135if (!!this.splitView) {136this.updateState();137this.resizeSplitViews();138}139}140141private updateState() {142if (!!this.splitView) {143this.state.targetWidth = this.splitView.getViewSize(1);144this.state.leftMarginRatio = this.splitView.getViewSize(0) / this.lastLayoutPosition.width;145this.state.rightMarginRatio = this.splitView.getViewSize(2) / this.lastLayoutPosition.width;146}147}148149isActive(): boolean {150return !!this.splitView;151}152153styles(style: ICenteredViewStyles): void {154this.style = style;155if (this.splitView && this.emptyViews) {156this.splitView.style(this.style);157this.emptyViews[0].element.style.backgroundColor = this.style.background.toString();158this.emptyViews[1].element.style.backgroundColor = this.style.background.toString();159}160}161162activate(active: boolean): void {163if (active === this.isActive()) {164return;165}166167if (active) {168this.view.element.remove();169this.splitView = new SplitView(this.container, {170inverseAltBehavior: true,171orientation: Orientation.HORIZONTAL,172styles: this.style173});174this.splitView.orthogonalStartSash = this.boundarySashes.top;175this.splitView.orthogonalEndSash = this.boundarySashes.bottom;176177this.splitViewDisposables.add(this.splitView.onDidSashChange(() => {178if (!!this.splitView) {179this.updateState();180}181}));182this.splitViewDisposables.add(this.splitView.onDidSashReset(() => {183this.state = { ...defaultState };184this.resizeSplitViews();185}));186187this.splitView.layout(this.lastLayoutPosition.width, this.lastLayoutPosition);188const backgroundColor = this.style ? this.style.background : undefined;189this.emptyViews = [createEmptyView(backgroundColor), createEmptyView(backgroundColor)];190191this.splitView.addView(this.emptyViews[0], distributeSizing, 0);192this.splitView.addView(toSplitViewView(this.view, () => this.lastLayoutPosition.height), distributeSizing, 1);193this.splitView.addView(this.emptyViews[1], distributeSizing, 2);194195this.resizeSplitViews();196} else {197this.splitView?.el.remove();198this.splitViewDisposables.clear();199this.splitView?.dispose();200this.splitView = undefined;201this.emptyViews = undefined;202this.container.appendChild(this.view.element);203this.view.layout(this.lastLayoutPosition.width, this.lastLayoutPosition.height, this.lastLayoutPosition.top, this.lastLayoutPosition.left);204}205}206207isDefault(state: CenteredViewState): boolean {208if (this.centeredLayoutFixedWidth) {209return state.targetWidth === defaultState.targetWidth;210} else {211return state.leftMarginRatio === defaultState.leftMarginRatio212&& state.rightMarginRatio === defaultState.rightMarginRatio;213}214}215216dispose(): void {217this.splitViewDisposables.dispose();218219if (this.splitView) {220this.splitView.dispose();221this.splitView = undefined;222}223}224}225226227