monaco-editor-core
Version:
A browser based code editor
827 lines (826 loc) • 37.9 kB
JavaScript
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { $, addDisposableListener, append, getWindow, scheduleAtNextAnimationFrame } from '../../dom.js';
import { DomEmitter } from '../../event.js';
import { Sash } from '../sash/sash.js';
import { SmoothScrollableElement } from '../scrollbar/scrollableElement.js';
import { pushToEnd, pushToStart, range } from '../../../common/arrays.js';
import { Color } from '../../../common/color.js';
import { Emitter, Event } from '../../../common/event.js';
import { combinedDisposable, Disposable, dispose, toDisposable } from '../../../common/lifecycle.js';
import { clamp } from '../../../common/numbers.js';
import { Scrollable } from '../../../common/scrollable.js';
import * as types from '../../../common/types.js';
import './splitview.css';
const defaultStyles = {
separatorBorder: Color.transparent
};
class ViewItem {
set size(size) {
this._size = size;
}
get size() {
return this._size;
}
get visible() {
return typeof this._cachedVisibleSize === 'undefined';
}
setVisible(visible, size) {
if (visible === this.visible) {
return;
}
if (visible) {
this.size = clamp(this._cachedVisibleSize, this.viewMinimumSize, this.viewMaximumSize);
this._cachedVisibleSize = undefined;
}
else {
this._cachedVisibleSize = typeof size === 'number' ? size : this.size;
this.size = 0;
}
this.container.classList.toggle('visible', visible);
try {
this.view.setVisible?.(visible);
}
catch (e) {
console.error('Splitview: Failed to set visible view');
console.error(e);
}
}
get minimumSize() { return this.visible ? this.view.minimumSize : 0; }
get viewMinimumSize() { return this.view.minimumSize; }
get maximumSize() { return this.visible ? this.view.maximumSize : 0; }
get viewMaximumSize() { return this.view.maximumSize; }
get priority() { return this.view.priority; }
get proportionalLayout() { return this.view.proportionalLayout ?? true; }
get snap() { return !!this.view.snap; }
set enabled(enabled) {
this.container.style.pointerEvents = enabled ? '' : 'none';
}
constructor(container, view, size, disposable) {
this.container = container;
this.view = view;
this.disposable = disposable;
this._cachedVisibleSize = undefined;
if (typeof size === 'number') {
this._size = size;
this._cachedVisibleSize = undefined;
container.classList.add('visible');
}
else {
this._size = 0;
this._cachedVisibleSize = size.cachedVisibleSize;
}
}
layout(offset, layoutContext) {
this.layoutContainer(offset);
try {
this.view.layout(this.size, offset, layoutContext);
}
catch (e) {
console.error('Splitview: Failed to layout view');
console.error(e);
}
}
dispose() {
this.disposable.dispose();
}
}
class VerticalViewItem extends ViewItem {
layoutContainer(offset) {
this.container.style.top = `${offset}px`;
this.container.style.height = `${this.size}px`;
}
}
class HorizontalViewItem extends ViewItem {
layoutContainer(offset) {
this.container.style.left = `${offset}px`;
this.container.style.width = `${this.size}px`;
}
}
var State;
(function (State) {
State[State["Idle"] = 0] = "Idle";
State[State["Busy"] = 1] = "Busy";
})(State || (State = {}));
export var Sizing;
(function (Sizing) {
/**
* When adding or removing views, distribute the delta space among
* all other views.
*/
Sizing.Distribute = { type: 'distribute' };
/**
* When adding or removing views, split the delta space with another
* specific view, indexed by the provided `index`.
*/
function Split(index) { return { type: 'split', index }; }
Sizing.Split = Split;
/**
* When adding a view, use DistributeSizing when all pre-existing views are
* distributed evenly, otherwise use SplitSizing.
*/
function Auto(index) { return { type: 'auto', index }; }
Sizing.Auto = Auto;
/**
* When adding or removing views, assume the view is invisible.
*/
function Invisible(cachedVisibleSize) { return { type: 'invisible', cachedVisibleSize }; }
Sizing.Invisible = Invisible;
})(Sizing || (Sizing = {}));
/**
* The {@link SplitView} is the UI component which implements a one dimensional
* flex-like layout algorithm for a collection of {@link IView} instances, which
* are essentially HTMLElement instances with the following size constraints:
*
* - {@link IView.minimumSize}
* - {@link IView.maximumSize}
* - {@link IView.priority}
* - {@link IView.snap}
*
* In case the SplitView doesn't have enough size to fit all views, it will overflow
* its content with a scrollbar.
*
* In between each pair of views there will be a {@link Sash} allowing the user
* to resize the views, making sure the constraints are respected.
*
* An optional {@link TLayoutContext layout context type} may be used in order to
* pass along layout contextual data from the {@link SplitView.layout} method down
* to each view's {@link IView.layout} calls.
*
* Features:
* - Flex-like layout algorithm
* - Snap support
* - Orthogonal sash support, for corner sashes
* - View hide/show support
* - View swap/move support
* - Alt key modifier behavior, macOS style
*/
export class SplitView extends Disposable {
get orthogonalStartSash() { return this._orthogonalStartSash; }
get orthogonalEndSash() { return this._orthogonalEndSash; }
get startSnappingEnabled() { return this._startSnappingEnabled; }
get endSnappingEnabled() { return this._endSnappingEnabled; }
/**
* A reference to a sash, perpendicular to all sashes in this {@link SplitView},
* located at the left- or top-most side of the SplitView.
* Corner sashes will be created automatically at the intersections.
*/
set orthogonalStartSash(sash) {
for (const sashItem of this.sashItems) {
sashItem.sash.orthogonalStartSash = sash;
}
this._orthogonalStartSash = sash;
}
/**
* A reference to a sash, perpendicular to all sashes in this {@link SplitView},
* located at the right- or bottom-most side of the SplitView.
* Corner sashes will be created automatically at the intersections.
*/
set orthogonalEndSash(sash) {
for (const sashItem of this.sashItems) {
sashItem.sash.orthogonalEndSash = sash;
}
this._orthogonalEndSash = sash;
}
/**
* Enable/disable snapping at the beginning of this {@link SplitView}.
*/
set startSnappingEnabled(startSnappingEnabled) {
if (this._startSnappingEnabled === startSnappingEnabled) {
return;
}
this._startSnappingEnabled = startSnappingEnabled;
this.updateSashEnablement();
}
/**
* Enable/disable snapping at the end of this {@link SplitView}.
*/
set endSnappingEnabled(endSnappingEnabled) {
if (this._endSnappingEnabled === endSnappingEnabled) {
return;
}
this._endSnappingEnabled = endSnappingEnabled;
this.updateSashEnablement();
}
/**
* Create a new {@link SplitView} instance.
*/
constructor(container, options = {}) {
super();
this.size = 0;
this._contentSize = 0;
this.proportions = undefined;
this.viewItems = [];
this.sashItems = []; // used in tests
this.state = State.Idle;
this._onDidSashChange = this._register(new Emitter());
this._onDidSashReset = this._register(new Emitter());
this._startSnappingEnabled = true;
this._endSnappingEnabled = true;
/**
* Fires whenever the user resizes a {@link Sash sash}.
*/
this.onDidSashChange = this._onDidSashChange.event;
/**
* Fires whenever the user double clicks a {@link Sash sash}.
*/
this.onDidSashReset = this._onDidSashReset.event;
this.orientation = options.orientation ?? 0 /* Orientation.VERTICAL */;
this.inverseAltBehavior = options.inverseAltBehavior ?? false;
this.proportionalLayout = options.proportionalLayout ?? true;
this.getSashOrthogonalSize = options.getSashOrthogonalSize;
this.el = document.createElement('div');
this.el.classList.add('monaco-split-view2');
this.el.classList.add(this.orientation === 0 /* Orientation.VERTICAL */ ? 'vertical' : 'horizontal');
container.appendChild(this.el);
this.sashContainer = append(this.el, $('.sash-container'));
this.viewContainer = $('.split-view-container');
this.scrollable = this._register(new Scrollable({
forceIntegerValues: true,
smoothScrollDuration: 125,
scheduleAtNextAnimationFrame: callback => scheduleAtNextAnimationFrame(getWindow(this.el), callback),
}));
this.scrollableElement = this._register(new SmoothScrollableElement(this.viewContainer, {
vertical: this.orientation === 0 /* Orientation.VERTICAL */ ? (options.scrollbarVisibility ?? 1 /* ScrollbarVisibility.Auto */) : 2 /* ScrollbarVisibility.Hidden */,
horizontal: this.orientation === 1 /* Orientation.HORIZONTAL */ ? (options.scrollbarVisibility ?? 1 /* ScrollbarVisibility.Auto */) : 2 /* ScrollbarVisibility.Hidden */
}, this.scrollable));
// https://github.com/microsoft/vscode/issues/157737
const onDidScrollViewContainer = this._register(new DomEmitter(this.viewContainer, 'scroll')).event;
this._register(onDidScrollViewContainer(_ => {
const position = this.scrollableElement.getScrollPosition();
const scrollLeft = Math.abs(this.viewContainer.scrollLeft - position.scrollLeft) <= 1 ? undefined : this.viewContainer.scrollLeft;
const scrollTop = Math.abs(this.viewContainer.scrollTop - position.scrollTop) <= 1 ? undefined : this.viewContainer.scrollTop;
if (scrollLeft !== undefined || scrollTop !== undefined) {
this.scrollableElement.setScrollPosition({ scrollLeft, scrollTop });
}
}));
this.onDidScroll = this.scrollableElement.onScroll;
this._register(this.onDidScroll(e => {
if (e.scrollTopChanged) {
this.viewContainer.scrollTop = e.scrollTop;
}
if (e.scrollLeftChanged) {
this.viewContainer.scrollLeft = e.scrollLeft;
}
}));
append(this.el, this.scrollableElement.getDomNode());
this.style(options.styles || defaultStyles);
// We have an existing set of view, add them now
if (options.descriptor) {
this.size = options.descriptor.size;
options.descriptor.views.forEach((viewDescriptor, index) => {
const sizing = types.isUndefined(viewDescriptor.visible) || viewDescriptor.visible ? viewDescriptor.size : { type: 'invisible', cachedVisibleSize: viewDescriptor.size };
const view = viewDescriptor.view;
this.doAddView(view, sizing, index, true);
});
// Initialize content size and proportions for first layout
this._contentSize = this.viewItems.reduce((r, i) => r + i.size, 0);
this.saveProportions();
}
}
style(styles) {
if (styles.separatorBorder.isTransparent()) {
this.el.classList.remove('separator-border');
this.el.style.removeProperty('--separator-border');
}
else {
this.el.classList.add('separator-border');
this.el.style.setProperty('--separator-border', styles.separatorBorder.toString());
}
}
/**
* Add a {@link IView view} to this {@link SplitView}.
*
* @param view The view to add.
* @param size Either a fixed size, or a dynamic {@link Sizing} strategy.
* @param index The index to insert the view on.
* @param skipLayout Whether layout should be skipped.
*/
addView(view, size, index = this.viewItems.length, skipLayout) {
this.doAddView(view, size, index, skipLayout);
}
/**
* Layout the {@link SplitView}.
*
* @param size The entire size of the {@link SplitView}.
* @param layoutContext An optional layout context to pass along to {@link IView views}.
*/
layout(size, layoutContext) {
const previousSize = Math.max(this.size, this._contentSize);
this.size = size;
this.layoutContext = layoutContext;
if (!this.proportions) {
const indexes = range(this.viewItems.length);
const lowPriorityIndexes = indexes.filter(i => this.viewItems[i].priority === 1 /* LayoutPriority.Low */);
const highPriorityIndexes = indexes.filter(i => this.viewItems[i].priority === 2 /* LayoutPriority.High */);
this.resize(this.viewItems.length - 1, size - previousSize, undefined, lowPriorityIndexes, highPriorityIndexes);
}
else {
let total = 0;
for (let i = 0; i < this.viewItems.length; i++) {
const item = this.viewItems[i];
const proportion = this.proportions[i];
if (typeof proportion === 'number') {
total += proportion;
}
else {
size -= item.size;
}
}
for (let i = 0; i < this.viewItems.length; i++) {
const item = this.viewItems[i];
const proportion = this.proportions[i];
if (typeof proportion === 'number' && total > 0) {
item.size = clamp(Math.round(proportion * size / total), item.minimumSize, item.maximumSize);
}
}
}
this.distributeEmptySpace();
this.layoutViews();
}
saveProportions() {
if (this.proportionalLayout && this._contentSize > 0) {
this.proportions = this.viewItems.map(v => v.proportionalLayout && v.visible ? v.size / this._contentSize : undefined);
}
}
onSashStart({ sash, start, alt }) {
for (const item of this.viewItems) {
item.enabled = false;
}
const index = this.sashItems.findIndex(item => item.sash === sash);
// This way, we can press Alt while we resize a sash, macOS style!
const disposable = combinedDisposable(addDisposableListener(this.el.ownerDocument.body, 'keydown', e => resetSashDragState(this.sashDragState.current, e.altKey)), addDisposableListener(this.el.ownerDocument.body, 'keyup', () => resetSashDragState(this.sashDragState.current, false)));
const resetSashDragState = (start, alt) => {
const sizes = this.viewItems.map(i => i.size);
let minDelta = Number.NEGATIVE_INFINITY;
let maxDelta = Number.POSITIVE_INFINITY;
if (this.inverseAltBehavior) {
alt = !alt;
}
if (alt) {
// When we're using the last sash with Alt, we're resizing
// the view to the left/up, instead of right/down as usual
// Thus, we must do the inverse of the usual
const isLastSash = index === this.sashItems.length - 1;
if (isLastSash) {
const viewItem = this.viewItems[index];
minDelta = (viewItem.minimumSize - viewItem.size) / 2;
maxDelta = (viewItem.maximumSize - viewItem.size) / 2;
}
else {
const viewItem = this.viewItems[index + 1];
minDelta = (viewItem.size - viewItem.maximumSize) / 2;
maxDelta = (viewItem.size - viewItem.minimumSize) / 2;
}
}
let snapBefore;
let snapAfter;
if (!alt) {
const upIndexes = range(index, -1);
const downIndexes = range(index + 1, this.viewItems.length);
const minDeltaUp = upIndexes.reduce((r, i) => r + (this.viewItems[i].minimumSize - sizes[i]), 0);
const maxDeltaUp = upIndexes.reduce((r, i) => r + (this.viewItems[i].viewMaximumSize - sizes[i]), 0);
const maxDeltaDown = downIndexes.length === 0 ? Number.POSITIVE_INFINITY : downIndexes.reduce((r, i) => r + (sizes[i] - this.viewItems[i].minimumSize), 0);
const minDeltaDown = downIndexes.length === 0 ? Number.NEGATIVE_INFINITY : downIndexes.reduce((r, i) => r + (sizes[i] - this.viewItems[i].viewMaximumSize), 0);
const minDelta = Math.max(minDeltaUp, minDeltaDown);
const maxDelta = Math.min(maxDeltaDown, maxDeltaUp);
const snapBeforeIndex = this.findFirstSnapIndex(upIndexes);
const snapAfterIndex = this.findFirstSnapIndex(downIndexes);
if (typeof snapBeforeIndex === 'number') {
const viewItem = this.viewItems[snapBeforeIndex];
const halfSize = Math.floor(viewItem.viewMinimumSize / 2);
snapBefore = {
index: snapBeforeIndex,
limitDelta: viewItem.visible ? minDelta - halfSize : minDelta + halfSize,
size: viewItem.size
};
}
if (typeof snapAfterIndex === 'number') {
const viewItem = this.viewItems[snapAfterIndex];
const halfSize = Math.floor(viewItem.viewMinimumSize / 2);
snapAfter = {
index: snapAfterIndex,
limitDelta: viewItem.visible ? maxDelta + halfSize : maxDelta - halfSize,
size: viewItem.size
};
}
}
this.sashDragState = { start, current: start, index, sizes, minDelta, maxDelta, alt, snapBefore, snapAfter, disposable };
};
resetSashDragState(start, alt);
}
onSashChange({ current }) {
const { index, start, sizes, alt, minDelta, maxDelta, snapBefore, snapAfter } = this.sashDragState;
this.sashDragState.current = current;
const delta = current - start;
const newDelta = this.resize(index, delta, sizes, undefined, undefined, minDelta, maxDelta, snapBefore, snapAfter);
if (alt) {
const isLastSash = index === this.sashItems.length - 1;
const newSizes = this.viewItems.map(i => i.size);
const viewItemIndex = isLastSash ? index : index + 1;
const viewItem = this.viewItems[viewItemIndex];
const newMinDelta = viewItem.size - viewItem.maximumSize;
const newMaxDelta = viewItem.size - viewItem.minimumSize;
const resizeIndex = isLastSash ? index - 1 : index + 1;
this.resize(resizeIndex, -newDelta, newSizes, undefined, undefined, newMinDelta, newMaxDelta);
}
this.distributeEmptySpace();
this.layoutViews();
}
onSashEnd(index) {
this._onDidSashChange.fire(index);
this.sashDragState.disposable.dispose();
this.saveProportions();
for (const item of this.viewItems) {
item.enabled = true;
}
}
onViewChange(item, size) {
const index = this.viewItems.indexOf(item);
if (index < 0 || index >= this.viewItems.length) {
return;
}
size = typeof size === 'number' ? size : item.size;
size = clamp(size, item.minimumSize, item.maximumSize);
if (this.inverseAltBehavior && index > 0) {
// In this case, we want the view to grow or shrink both sides equally
// so we just resize the "left" side by half and let `resize` do the clamping magic
this.resize(index - 1, Math.floor((item.size - size) / 2));
this.distributeEmptySpace();
this.layoutViews();
}
else {
item.size = size;
this.relayout([index], undefined);
}
}
/**
* Resize a {@link IView view} within the {@link SplitView}.
*
* @param index The {@link IView view} index.
* @param size The {@link IView view} size.
*/
resizeView(index, size) {
if (index < 0 || index >= this.viewItems.length) {
return;
}
if (this.state !== State.Idle) {
throw new Error('Cant modify splitview');
}
this.state = State.Busy;
try {
const indexes = range(this.viewItems.length).filter(i => i !== index);
const lowPriorityIndexes = [...indexes.filter(i => this.viewItems[i].priority === 1 /* LayoutPriority.Low */), index];
const highPriorityIndexes = indexes.filter(i => this.viewItems[i].priority === 2 /* LayoutPriority.High */);
const item = this.viewItems[index];
size = Math.round(size);
size = clamp(size, item.minimumSize, Math.min(item.maximumSize, this.size));
item.size = size;
this.relayout(lowPriorityIndexes, highPriorityIndexes);
}
finally {
this.state = State.Idle;
}
}
/**
* Distribute the entire {@link SplitView} size among all {@link IView views}.
*/
distributeViewSizes() {
const flexibleViewItems = [];
let flexibleSize = 0;
for (const item of this.viewItems) {
if (item.maximumSize - item.minimumSize > 0) {
flexibleViewItems.push(item);
flexibleSize += item.size;
}
}
const size = Math.floor(flexibleSize / flexibleViewItems.length);
for (const item of flexibleViewItems) {
item.size = clamp(size, item.minimumSize, item.maximumSize);
}
const indexes = range(this.viewItems.length);
const lowPriorityIndexes = indexes.filter(i => this.viewItems[i].priority === 1 /* LayoutPriority.Low */);
const highPriorityIndexes = indexes.filter(i => this.viewItems[i].priority === 2 /* LayoutPriority.High */);
this.relayout(lowPriorityIndexes, highPriorityIndexes);
}
/**
* Returns the size of a {@link IView view}.
*/
getViewSize(index) {
if (index < 0 || index >= this.viewItems.length) {
return -1;
}
return this.viewItems[index].size;
}
doAddView(view, size, index = this.viewItems.length, skipLayout) {
if (this.state !== State.Idle) {
throw new Error('Cant modify splitview');
}
this.state = State.Busy;
try {
// Add view
const container = $('.split-view-view');
if (index === this.viewItems.length) {
this.viewContainer.appendChild(container);
}
else {
this.viewContainer.insertBefore(container, this.viewContainer.children.item(index));
}
const onChangeDisposable = view.onDidChange(size => this.onViewChange(item, size));
const containerDisposable = toDisposable(() => container.remove());
const disposable = combinedDisposable(onChangeDisposable, containerDisposable);
let viewSize;
if (typeof size === 'number') {
viewSize = size;
}
else {
if (size.type === 'auto') {
if (this.areViewsDistributed()) {
size = { type: 'distribute' };
}
else {
size = { type: 'split', index: size.index };
}
}
if (size.type === 'split') {
viewSize = this.getViewSize(size.index) / 2;
}
else if (size.type === 'invisible') {
viewSize = { cachedVisibleSize: size.cachedVisibleSize };
}
else {
viewSize = view.minimumSize;
}
}
const item = this.orientation === 0 /* Orientation.VERTICAL */
? new VerticalViewItem(container, view, viewSize, disposable)
: new HorizontalViewItem(container, view, viewSize, disposable);
this.viewItems.splice(index, 0, item);
// Add sash
if (this.viewItems.length > 1) {
const opts = { orthogonalStartSash: this.orthogonalStartSash, orthogonalEndSash: this.orthogonalEndSash };
const sash = this.orientation === 0 /* Orientation.VERTICAL */
? new Sash(this.sashContainer, { getHorizontalSashTop: s => this.getSashPosition(s), getHorizontalSashWidth: this.getSashOrthogonalSize }, { ...opts, orientation: 1 /* Orientation.HORIZONTAL */ })
: new Sash(this.sashContainer, { getVerticalSashLeft: s => this.getSashPosition(s), getVerticalSashHeight: this.getSashOrthogonalSize }, { ...opts, orientation: 0 /* Orientation.VERTICAL */ });
const sashEventMapper = this.orientation === 0 /* Orientation.VERTICAL */
? (e) => ({ sash, start: e.startY, current: e.currentY, alt: e.altKey })
: (e) => ({ sash, start: e.startX, current: e.currentX, alt: e.altKey });
const onStart = Event.map(sash.onDidStart, sashEventMapper);
const onStartDisposable = onStart(this.onSashStart, this);
const onChange = Event.map(sash.onDidChange, sashEventMapper);
const onChangeDisposable = onChange(this.onSashChange, this);
const onEnd = Event.map(sash.onDidEnd, () => this.sashItems.findIndex(item => item.sash === sash));
const onEndDisposable = onEnd(this.onSashEnd, this);
const onDidResetDisposable = sash.onDidReset(() => {
const index = this.sashItems.findIndex(item => item.sash === sash);
const upIndexes = range(index, -1);
const downIndexes = range(index + 1, this.viewItems.length);
const snapBeforeIndex = this.findFirstSnapIndex(upIndexes);
const snapAfterIndex = this.findFirstSnapIndex(downIndexes);
if (typeof snapBeforeIndex === 'number' && !this.viewItems[snapBeforeIndex].visible) {
return;
}
if (typeof snapAfterIndex === 'number' && !this.viewItems[snapAfterIndex].visible) {
return;
}
this._onDidSashReset.fire(index);
});
const disposable = combinedDisposable(onStartDisposable, onChangeDisposable, onEndDisposable, onDidResetDisposable, sash);
const sashItem = { sash, disposable };
this.sashItems.splice(index - 1, 0, sashItem);
}
container.appendChild(view.element);
let highPriorityIndexes;
if (typeof size !== 'number' && size.type === 'split') {
highPriorityIndexes = [size.index];
}
if (!skipLayout) {
this.relayout([index], highPriorityIndexes);
}
if (!skipLayout && typeof size !== 'number' && size.type === 'distribute') {
this.distributeViewSizes();
}
}
finally {
this.state = State.Idle;
}
}
relayout(lowPriorityIndexes, highPriorityIndexes) {
const contentSize = this.viewItems.reduce((r, i) => r + i.size, 0);
this.resize(this.viewItems.length - 1, this.size - contentSize, undefined, lowPriorityIndexes, highPriorityIndexes);
this.distributeEmptySpace();
this.layoutViews();
this.saveProportions();
}
resize(index, delta, sizes = this.viewItems.map(i => i.size), lowPriorityIndexes, highPriorityIndexes, overloadMinDelta = Number.NEGATIVE_INFINITY, overloadMaxDelta = Number.POSITIVE_INFINITY, snapBefore, snapAfter) {
if (index < 0 || index >= this.viewItems.length) {
return 0;
}
const upIndexes = range(index, -1);
const downIndexes = range(index + 1, this.viewItems.length);
if (highPriorityIndexes) {
for (const index of highPriorityIndexes) {
pushToStart(upIndexes, index);
pushToStart(downIndexes, index);
}
}
if (lowPriorityIndexes) {
for (const index of lowPriorityIndexes) {
pushToEnd(upIndexes, index);
pushToEnd(downIndexes, index);
}
}
const upItems = upIndexes.map(i => this.viewItems[i]);
const upSizes = upIndexes.map(i => sizes[i]);
const downItems = downIndexes.map(i => this.viewItems[i]);
const downSizes = downIndexes.map(i => sizes[i]);
const minDeltaUp = upIndexes.reduce((r, i) => r + (this.viewItems[i].minimumSize - sizes[i]), 0);
const maxDeltaUp = upIndexes.reduce((r, i) => r + (this.viewItems[i].maximumSize - sizes[i]), 0);
const maxDeltaDown = downIndexes.length === 0 ? Number.POSITIVE_INFINITY : downIndexes.reduce((r, i) => r + (sizes[i] - this.viewItems[i].minimumSize), 0);
const minDeltaDown = downIndexes.length === 0 ? Number.NEGATIVE_INFINITY : downIndexes.reduce((r, i) => r + (sizes[i] - this.viewItems[i].maximumSize), 0);
const minDelta = Math.max(minDeltaUp, minDeltaDown, overloadMinDelta);
const maxDelta = Math.min(maxDeltaDown, maxDeltaUp, overloadMaxDelta);
let snapped = false;
if (snapBefore) {
const snapView = this.viewItems[snapBefore.index];
const visible = delta >= snapBefore.limitDelta;
snapped = visible !== snapView.visible;
snapView.setVisible(visible, snapBefore.size);
}
if (!snapped && snapAfter) {
const snapView = this.viewItems[snapAfter.index];
const visible = delta < snapAfter.limitDelta;
snapped = visible !== snapView.visible;
snapView.setVisible(visible, snapAfter.size);
}
if (snapped) {
return this.resize(index, delta, sizes, lowPriorityIndexes, highPriorityIndexes, overloadMinDelta, overloadMaxDelta);
}
delta = clamp(delta, minDelta, maxDelta);
for (let i = 0, deltaUp = delta; i < upItems.length; i++) {
const item = upItems[i];
const size = clamp(upSizes[i] + deltaUp, item.minimumSize, item.maximumSize);
const viewDelta = size - upSizes[i];
deltaUp -= viewDelta;
item.size = size;
}
for (let i = 0, deltaDown = delta; i < downItems.length; i++) {
const item = downItems[i];
const size = clamp(downSizes[i] - deltaDown, item.minimumSize, item.maximumSize);
const viewDelta = size - downSizes[i];
deltaDown += viewDelta;
item.size = size;
}
return delta;
}
distributeEmptySpace(lowPriorityIndex) {
const contentSize = this.viewItems.reduce((r, i) => r + i.size, 0);
let emptyDelta = this.size - contentSize;
const indexes = range(this.viewItems.length - 1, -1);
const lowPriorityIndexes = indexes.filter(i => this.viewItems[i].priority === 1 /* LayoutPriority.Low */);
const highPriorityIndexes = indexes.filter(i => this.viewItems[i].priority === 2 /* LayoutPriority.High */);
for (const index of highPriorityIndexes) {
pushToStart(indexes, index);
}
for (const index of lowPriorityIndexes) {
pushToEnd(indexes, index);
}
if (typeof lowPriorityIndex === 'number') {
pushToEnd(indexes, lowPriorityIndex);
}
for (let i = 0; emptyDelta !== 0 && i < indexes.length; i++) {
const item = this.viewItems[indexes[i]];
const size = clamp(item.size + emptyDelta, item.minimumSize, item.maximumSize);
const viewDelta = size - item.size;
emptyDelta -= viewDelta;
item.size = size;
}
}
layoutViews() {
// Save new content size
this._contentSize = this.viewItems.reduce((r, i) => r + i.size, 0);
// Layout views
let offset = 0;
for (const viewItem of this.viewItems) {
viewItem.layout(offset, this.layoutContext);
offset += viewItem.size;
}
// Layout sashes
this.sashItems.forEach(item => item.sash.layout());
this.updateSashEnablement();
this.updateScrollableElement();
}
updateScrollableElement() {
if (this.orientation === 0 /* Orientation.VERTICAL */) {
this.scrollableElement.setScrollDimensions({
height: this.size,
scrollHeight: this._contentSize
});
}
else {
this.scrollableElement.setScrollDimensions({
width: this.size,
scrollWidth: this._contentSize
});
}
}
updateSashEnablement() {
let previous = false;
const collapsesDown = this.viewItems.map(i => previous = (i.size - i.minimumSize > 0) || previous);
previous = false;
const expandsDown = this.viewItems.map(i => previous = (i.maximumSize - i.size > 0) || previous);
const reverseViews = [...this.viewItems].reverse();
previous = false;
const collapsesUp = reverseViews.map(i => previous = (i.size - i.minimumSize > 0) || previous).reverse();
previous = false;
const expandsUp = reverseViews.map(i => previous = (i.maximumSize - i.size > 0) || previous).reverse();
let position = 0;
for (let index = 0; index < this.sashItems.length; index++) {
const { sash } = this.sashItems[index];
const viewItem = this.viewItems[index];
position += viewItem.size;
const min = !(collapsesDown[index] && expandsUp[index + 1]);
const max = !(expandsDown[index] && collapsesUp[index + 1]);
if (min && max) {
const upIndexes = range(index, -1);
const downIndexes = range(index + 1, this.viewItems.length);
const snapBeforeIndex = this.findFirstSnapIndex(upIndexes);
const snapAfterIndex = this.findFirstSnapIndex(downIndexes);
const snappedBefore = typeof snapBeforeIndex === 'number' && !this.viewItems[snapBeforeIndex].visible;
const snappedAfter = typeof snapAfterIndex === 'number' && !this.viewItems[snapAfterIndex].visible;
if (snappedBefore && collapsesUp[index] && (position > 0 || this.startSnappingEnabled)) {
sash.state = 1 /* SashState.AtMinimum */;
}
else if (snappedAfter && collapsesDown[index] && (position < this._contentSize || this.endSnappingEnabled)) {
sash.state = 2 /* SashState.AtMaximum */;
}
else {
sash.state = 0 /* SashState.Disabled */;
}
}
else if (min && !max) {
sash.state = 1 /* SashState.AtMinimum */;
}
else if (!min && max) {
sash.state = 2 /* SashState.AtMaximum */;
}
else {
sash.state = 3 /* SashState.Enabled */;
}
}
}
getSashPosition(sash) {
let position = 0;
for (let i = 0; i < this.sashItems.length; i++) {
position += this.viewItems[i].size;
if (this.sashItems[i].sash === sash) {
return position;
}
}
return 0;
}
findFirstSnapIndex(indexes) {
// visible views first
for (const index of indexes) {
const viewItem = this.viewItems[index];
if (!viewItem.visible) {
continue;
}
if (viewItem.snap) {
return index;
}
}
// then, hidden views
for (const index of indexes) {
const viewItem = this.viewItems[index];
if (viewItem.visible && viewItem.maximumSize - viewItem.minimumSize > 0) {
return undefined;
}
if (!viewItem.visible && viewItem.snap) {
return index;
}
}
return undefined;
}
areViewsDistributed() {
let min = undefined, max = undefined;
for (const view of this.viewItems) {
min = min === undefined ? view.size : Math.min(min, view.size);
max = max === undefined ? view.size : Math.max(max, view.size);
if (max - min > 2) {
return false;
}
}
return true;
}
dispose() {
this.sashDragState?.disposable.dispose();
dispose(this.viewItems);
this.viewItems = [];
this.sashItems.forEach(i => i.disposable.dispose());
this.sashItems = [];
super.dispose();
}
}