Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/base/browser/ui/splitview/splitview.ts
3296 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import { $, addDisposableListener, append, getWindow, scheduleAtNextAnimationFrame } from '../../dom.js';
7
import { DomEmitter } from '../../event.js';
8
import { ISashEvent as IBaseSashEvent, Orientation, Sash, SashState } from '../sash/sash.js';
9
import { SmoothScrollableElement } from '../scrollbar/scrollableElement.js';
10
import { pushToEnd, pushToStart, range } from '../../../common/arrays.js';
11
import { Color } from '../../../common/color.js';
12
import { Emitter, Event } from '../../../common/event.js';
13
import { combinedDisposable, Disposable, dispose, IDisposable, toDisposable } from '../../../common/lifecycle.js';
14
import { clamp } from '../../../common/numbers.js';
15
import { Scrollable, ScrollbarVisibility, ScrollEvent } from '../../../common/scrollable.js';
16
import * as types from '../../../common/types.js';
17
import './splitview.css';
18
export { Orientation } from '../sash/sash.js';
19
20
export interface ISplitViewStyles {
21
readonly separatorBorder: Color;
22
}
23
24
const defaultStyles: ISplitViewStyles = {
25
separatorBorder: Color.transparent
26
};
27
28
export const enum LayoutPriority {
29
Normal,
30
Low,
31
High
32
}
33
34
/**
35
* The interface to implement for views within a {@link SplitView}.
36
*
37
* An optional {@link TLayoutContext layout context type} may be used in order to
38
* pass along layout contextual data from the {@link SplitView.layout} method down
39
* to each view's {@link IView.layout} calls.
40
*/
41
export interface IView<TLayoutContext = undefined> {
42
43
/**
44
* The DOM element for this view.
45
*/
46
readonly element: HTMLElement;
47
48
/**
49
* A minimum size for this view.
50
*
51
* @remarks If none, set it to `0`.
52
*/
53
readonly minimumSize: number;
54
55
/**
56
* A maximum size for this view.
57
*
58
* @remarks If none, set it to `Number.POSITIVE_INFINITY`.
59
*/
60
readonly maximumSize: number;
61
62
/**
63
* The priority of the view when the {@link SplitView.resize layout} algorithm
64
* runs. Views with higher priority will be resized first.
65
*
66
* @remarks Only used when `proportionalLayout` is false.
67
*/
68
readonly priority?: LayoutPriority;
69
70
/**
71
* If the {@link SplitView} supports {@link ISplitViewOptions.proportionalLayout proportional layout},
72
* this property allows for finer control over the proportional layout algorithm, per view.
73
*
74
* @defaultValue `true`
75
*/
76
readonly proportionalLayout?: boolean;
77
78
/**
79
* Whether the view will snap whenever the user reaches its minimum size or
80
* attempts to grow it beyond the minimum size.
81
*
82
* @defaultValue `false`
83
*/
84
readonly snap?: boolean;
85
86
/**
87
* View instances are supposed to fire the {@link IView.onDidChange} event whenever
88
* any of the constraint properties have changed:
89
*
90
* - {@link IView.minimumSize}
91
* - {@link IView.maximumSize}
92
* - {@link IView.priority}
93
* - {@link IView.snap}
94
*
95
* The SplitView will relayout whenever that happens. The event can optionally emit
96
* the view's preferred size for that relayout.
97
*/
98
readonly onDidChange: Event<number | undefined>;
99
100
/**
101
* This will be called by the {@link SplitView} during layout. A view meant to
102
* pass along the layout information down to its descendants.
103
*
104
* @param size The size of this view, in pixels.
105
* @param offset The offset of this view, relative to the start of the {@link SplitView}.
106
* @param context The optional {@link IView layout context} passed to {@link SplitView.layout}.
107
*/
108
layout(size: number, offset: number, context: TLayoutContext | undefined): void;
109
110
/**
111
* This will be called by the {@link SplitView} whenever this view is made
112
* visible or hidden.
113
*
114
* @param visible Whether the view becomes visible.
115
*/
116
setVisible?(visible: boolean): void;
117
}
118
119
/**
120
* A descriptor for a {@link SplitView} instance.
121
*/
122
export interface ISplitViewDescriptor<TLayoutContext = undefined, TView extends IView<TLayoutContext> = IView<TLayoutContext>> {
123
124
/**
125
* The layout size of the {@link SplitView}.
126
*/
127
readonly size: number;
128
129
/**
130
* Descriptors for each {@link IView view}.
131
*/
132
readonly views: {
133
134
/**
135
* Whether the {@link IView view} is visible.
136
*
137
* @defaultValue `true`
138
*/
139
readonly visible?: boolean;
140
141
/**
142
* The size of the {@link IView view}.
143
*
144
* @defaultValue `true`
145
*/
146
readonly size: number;
147
148
/**
149
* The size of the {@link IView view}.
150
*
151
* @defaultValue `true`
152
*/
153
readonly view: TView;
154
}[];
155
}
156
157
export interface ISplitViewOptions<TLayoutContext = undefined, TView extends IView<TLayoutContext> = IView<TLayoutContext>> {
158
159
/**
160
* Which axis the views align on.
161
*
162
* @defaultValue `Orientation.VERTICAL`
163
*/
164
readonly orientation?: Orientation;
165
166
/**
167
* Styles overriding the {@link defaultStyles default ones}.
168
*/
169
readonly styles?: ISplitViewStyles;
170
171
/**
172
* Make Alt-drag the default drag operation.
173
*/
174
readonly inverseAltBehavior?: boolean;
175
176
/**
177
* Resize each view proportionally when resizing the SplitView.
178
*
179
* @defaultValue `true`
180
*/
181
readonly proportionalLayout?: boolean;
182
183
/**
184
* An initial description of this {@link SplitView} instance, allowing
185
* to initialze all views within the ctor.
186
*/
187
readonly descriptor?: ISplitViewDescriptor<TLayoutContext, TView>;
188
189
/**
190
* The scrollbar visibility setting for whenever the views within
191
* the {@link SplitView} overflow.
192
*/
193
readonly scrollbarVisibility?: ScrollbarVisibility;
194
195
/**
196
* Override the orthogonal size of sashes.
197
*/
198
readonly getSashOrthogonalSize?: () => number;
199
}
200
201
interface ISashEvent {
202
readonly sash: Sash;
203
readonly start: number;
204
readonly current: number;
205
readonly alt: boolean;
206
}
207
208
type ViewItemSize = number | { cachedVisibleSize: number };
209
210
abstract class ViewItem<TLayoutContext, TView extends IView<TLayoutContext>> {
211
212
private _size: number;
213
set size(size: number) {
214
this._size = size;
215
}
216
217
get size(): number {
218
return this._size;
219
}
220
221
private _cachedVisibleSize: number | undefined = undefined;
222
get cachedVisibleSize(): number | undefined { return this._cachedVisibleSize; }
223
224
get visible(): boolean {
225
return typeof this._cachedVisibleSize === 'undefined';
226
}
227
228
setVisible(visible: boolean, size?: number): void {
229
if (visible === this.visible) {
230
return;
231
}
232
233
if (visible) {
234
this.size = clamp(this._cachedVisibleSize!, this.viewMinimumSize, this.viewMaximumSize);
235
this._cachedVisibleSize = undefined;
236
} else {
237
this._cachedVisibleSize = typeof size === 'number' ? size : this.size;
238
this.size = 0;
239
}
240
241
this.container.classList.toggle('visible', visible);
242
243
try {
244
this.view.setVisible?.(visible);
245
} catch (e) {
246
console.error('Splitview: Failed to set visible view');
247
console.error(e);
248
}
249
}
250
251
get minimumSize(): number { return this.visible ? this.view.minimumSize : 0; }
252
get viewMinimumSize(): number { return this.view.minimumSize; }
253
254
get maximumSize(): number { return this.visible ? this.view.maximumSize : 0; }
255
get viewMaximumSize(): number { return this.view.maximumSize; }
256
257
get priority(): LayoutPriority | undefined { return this.view.priority; }
258
get proportionalLayout(): boolean { return this.view.proportionalLayout ?? true; }
259
get snap(): boolean { return !!this.view.snap; }
260
261
set enabled(enabled: boolean) {
262
this.container.style.pointerEvents = enabled ? '' : 'none';
263
}
264
265
constructor(
266
protected container: HTMLElement,
267
readonly view: TView,
268
size: ViewItemSize,
269
private disposable: IDisposable
270
) {
271
if (typeof size === 'number') {
272
this._size = size;
273
this._cachedVisibleSize = undefined;
274
container.classList.add('visible');
275
} else {
276
this._size = 0;
277
this._cachedVisibleSize = size.cachedVisibleSize;
278
}
279
}
280
281
layout(offset: number, layoutContext: TLayoutContext | undefined): void {
282
this.layoutContainer(offset);
283
284
try {
285
this.view.layout(this.size, offset, layoutContext);
286
} catch (e) {
287
console.error('Splitview: Failed to layout view');
288
console.error(e);
289
}
290
}
291
292
abstract layoutContainer(offset: number): void;
293
294
dispose(): void {
295
this.disposable.dispose();
296
}
297
}
298
299
class VerticalViewItem<TLayoutContext, TView extends IView<TLayoutContext>> extends ViewItem<TLayoutContext, TView> {
300
301
layoutContainer(offset: number): void {
302
this.container.style.top = `${offset}px`;
303
this.container.style.height = `${this.size}px`;
304
}
305
}
306
307
class HorizontalViewItem<TLayoutContext, TView extends IView<TLayoutContext>> extends ViewItem<TLayoutContext, TView> {
308
309
layoutContainer(offset: number): void {
310
this.container.style.left = `${offset}px`;
311
this.container.style.width = `${this.size}px`;
312
}
313
}
314
315
interface ISashItem {
316
sash: Sash;
317
disposable: IDisposable;
318
}
319
320
interface ISashDragSnapState {
321
readonly index: number;
322
readonly limitDelta: number;
323
readonly size: number;
324
}
325
326
interface ISashDragState {
327
index: number;
328
start: number;
329
current: number;
330
sizes: number[];
331
minDelta: number;
332
maxDelta: number;
333
alt: boolean;
334
snapBefore: ISashDragSnapState | undefined;
335
snapAfter: ISashDragSnapState | undefined;
336
disposable: IDisposable;
337
}
338
339
enum State {
340
Idle,
341
Busy
342
}
343
344
/**
345
* When adding or removing views, uniformly distribute the entire split view space among
346
* all views.
347
*/
348
export type DistributeSizing = { type: 'distribute' };
349
350
/**
351
* When adding a view, make space for it by reducing the size of another view,
352
* indexed by the provided `index`.
353
*/
354
export type SplitSizing = { type: 'split'; index: number };
355
356
/**
357
* When adding a view, use DistributeSizing when all pre-existing views are
358
* distributed evenly, otherwise use SplitSizing.
359
*/
360
export type AutoSizing = { type: 'auto'; index: number };
361
362
/**
363
* When adding or removing views, assume the view is invisible.
364
*/
365
export type InvisibleSizing = { type: 'invisible'; cachedVisibleSize: number };
366
367
/**
368
* When adding or removing views, the sizing provides fine grained
369
* control over how other views get resized.
370
*/
371
export type Sizing = DistributeSizing | SplitSizing | AutoSizing | InvisibleSizing;
372
373
export namespace Sizing {
374
375
/**
376
* When adding or removing views, distribute the delta space among
377
* all other views.
378
*/
379
export const Distribute: DistributeSizing = { type: 'distribute' };
380
381
/**
382
* When adding or removing views, split the delta space with another
383
* specific view, indexed by the provided `index`.
384
*/
385
export function Split(index: number): SplitSizing { return { type: 'split', index }; }
386
387
/**
388
* When adding a view, use DistributeSizing when all pre-existing views are
389
* distributed evenly, otherwise use SplitSizing.
390
*/
391
export function Auto(index: number): AutoSizing { return { type: 'auto', index }; }
392
393
/**
394
* When adding or removing views, assume the view is invisible.
395
*/
396
export function Invisible(cachedVisibleSize: number): InvisibleSizing { return { type: 'invisible', cachedVisibleSize }; }
397
}
398
399
/**
400
* The {@link SplitView} is the UI component which implements a one dimensional
401
* flex-like layout algorithm for a collection of {@link IView} instances, which
402
* are essentially HTMLElement instances with the following size constraints:
403
*
404
* - {@link IView.minimumSize}
405
* - {@link IView.maximumSize}
406
* - {@link IView.priority}
407
* - {@link IView.snap}
408
*
409
* In case the SplitView doesn't have enough size to fit all views, it will overflow
410
* its content with a scrollbar.
411
*
412
* In between each pair of views there will be a {@link Sash} allowing the user
413
* to resize the views, making sure the constraints are respected.
414
*
415
* An optional {@link TLayoutContext layout context type} may be used in order to
416
* pass along layout contextual data from the {@link SplitView.layout} method down
417
* to each view's {@link IView.layout} calls.
418
*
419
* Features:
420
* - Flex-like layout algorithm
421
* - Snap support
422
* - Orthogonal sash support, for corner sashes
423
* - View hide/show support
424
* - View swap/move support
425
* - Alt key modifier behavior, macOS style
426
*/
427
export class SplitView<TLayoutContext = undefined, TView extends IView<TLayoutContext> = IView<TLayoutContext>> extends Disposable {
428
429
/**
430
* This {@link SplitView}'s orientation.
431
*/
432
readonly orientation: Orientation;
433
434
/**
435
* The DOM element representing this {@link SplitView}.
436
*/
437
readonly el: HTMLElement;
438
439
private sashContainer: HTMLElement;
440
private viewContainer: HTMLElement;
441
private scrollable: Scrollable;
442
private scrollableElement: SmoothScrollableElement;
443
private size = 0;
444
private layoutContext: TLayoutContext | undefined;
445
private _contentSize = 0;
446
private proportions: (number | undefined)[] | undefined = undefined;
447
private viewItems: ViewItem<TLayoutContext, TView>[] = [];
448
sashItems: ISashItem[] = []; // used in tests
449
private sashDragState: ISashDragState | undefined;
450
private state: State = State.Idle;
451
private inverseAltBehavior: boolean;
452
private proportionalLayout: boolean;
453
private readonly getSashOrthogonalSize: { (): number } | undefined;
454
455
private _onDidSashChange = this._register(new Emitter<number>());
456
private _onDidSashReset = this._register(new Emitter<number>());
457
private _orthogonalStartSash: Sash | undefined;
458
private _orthogonalEndSash: Sash | undefined;
459
private _startSnappingEnabled = true;
460
private _endSnappingEnabled = true;
461
462
/**
463
* The sum of all views' sizes.
464
*/
465
get contentSize(): number { return this._contentSize; }
466
467
/**
468
* Fires whenever the user resizes a {@link Sash sash}.
469
*/
470
readonly onDidSashChange = this._onDidSashChange.event;
471
472
/**
473
* Fires whenever the user double clicks a {@link Sash sash}.
474
*/
475
readonly onDidSashReset = this._onDidSashReset.event;
476
477
/**
478
* Fires whenever the split view is scrolled.
479
*/
480
readonly onDidScroll: Event<ScrollEvent>;
481
482
/**
483
* The amount of views in this {@link SplitView}.
484
*/
485
get length(): number {
486
return this.viewItems.length;
487
}
488
489
/**
490
* The minimum size of this {@link SplitView}.
491
*/
492
get minimumSize(): number {
493
return this.viewItems.reduce((r, item) => r + item.minimumSize, 0);
494
}
495
496
/**
497
* The maximum size of this {@link SplitView}.
498
*/
499
get maximumSize(): number {
500
return this.length === 0 ? Number.POSITIVE_INFINITY : this.viewItems.reduce((r, item) => r + item.maximumSize, 0);
501
}
502
503
get orthogonalStartSash(): Sash | undefined { return this._orthogonalStartSash; }
504
get orthogonalEndSash(): Sash | undefined { return this._orthogonalEndSash; }
505
get startSnappingEnabled(): boolean { return this._startSnappingEnabled; }
506
get endSnappingEnabled(): boolean { return this._endSnappingEnabled; }
507
508
/**
509
* A reference to a sash, perpendicular to all sashes in this {@link SplitView},
510
* located at the left- or top-most side of the SplitView.
511
* Corner sashes will be created automatically at the intersections.
512
*/
513
set orthogonalStartSash(sash: Sash | undefined) {
514
for (const sashItem of this.sashItems) {
515
sashItem.sash.orthogonalStartSash = sash;
516
}
517
518
this._orthogonalStartSash = sash;
519
}
520
521
/**
522
* A reference to a sash, perpendicular to all sashes in this {@link SplitView},
523
* located at the right- or bottom-most side of the SplitView.
524
* Corner sashes will be created automatically at the intersections.
525
*/
526
set orthogonalEndSash(sash: Sash | undefined) {
527
for (const sashItem of this.sashItems) {
528
sashItem.sash.orthogonalEndSash = sash;
529
}
530
531
this._orthogonalEndSash = sash;
532
}
533
534
/**
535
* The internal sashes within this {@link SplitView}.
536
*/
537
get sashes(): readonly Sash[] {
538
return this.sashItems.map(s => s.sash);
539
}
540
541
/**
542
* Enable/disable snapping at the beginning of this {@link SplitView}.
543
*/
544
set startSnappingEnabled(startSnappingEnabled: boolean) {
545
if (this._startSnappingEnabled === startSnappingEnabled) {
546
return;
547
}
548
549
this._startSnappingEnabled = startSnappingEnabled;
550
this.updateSashEnablement();
551
}
552
553
/**
554
* Enable/disable snapping at the end of this {@link SplitView}.
555
*/
556
set endSnappingEnabled(endSnappingEnabled: boolean) {
557
if (this._endSnappingEnabled === endSnappingEnabled) {
558
return;
559
}
560
561
this._endSnappingEnabled = endSnappingEnabled;
562
this.updateSashEnablement();
563
}
564
565
/**
566
* Create a new {@link SplitView} instance.
567
*/
568
constructor(container: HTMLElement, options: ISplitViewOptions<TLayoutContext, TView> = {}) {
569
super();
570
571
this.orientation = options.orientation ?? Orientation.VERTICAL;
572
this.inverseAltBehavior = options.inverseAltBehavior ?? false;
573
this.proportionalLayout = options.proportionalLayout ?? true;
574
this.getSashOrthogonalSize = options.getSashOrthogonalSize;
575
576
this.el = document.createElement('div');
577
this.el.classList.add('monaco-split-view2');
578
this.el.classList.add(this.orientation === Orientation.VERTICAL ? 'vertical' : 'horizontal');
579
container.appendChild(this.el);
580
581
this.sashContainer = append(this.el, $('.sash-container'));
582
this.viewContainer = $('.split-view-container');
583
584
this.scrollable = this._register(new Scrollable({
585
forceIntegerValues: true,
586
smoothScrollDuration: 125,
587
scheduleAtNextAnimationFrame: callback => scheduleAtNextAnimationFrame(getWindow(this.el), callback),
588
}));
589
this.scrollableElement = this._register(new SmoothScrollableElement(this.viewContainer, {
590
vertical: this.orientation === Orientation.VERTICAL ? (options.scrollbarVisibility ?? ScrollbarVisibility.Auto) : ScrollbarVisibility.Hidden,
591
horizontal: this.orientation === Orientation.HORIZONTAL ? (options.scrollbarVisibility ?? ScrollbarVisibility.Auto) : ScrollbarVisibility.Hidden
592
}, this.scrollable));
593
594
// https://github.com/microsoft/vscode/issues/157737
595
const onDidScrollViewContainer = this._register(new DomEmitter(this.viewContainer, 'scroll')).event;
596
this._register(onDidScrollViewContainer(_ => {
597
const position = this.scrollableElement.getScrollPosition();
598
const scrollLeft = Math.abs(this.viewContainer.scrollLeft - position.scrollLeft) <= 1 ? undefined : this.viewContainer.scrollLeft;
599
const scrollTop = Math.abs(this.viewContainer.scrollTop - position.scrollTop) <= 1 ? undefined : this.viewContainer.scrollTop;
600
601
if (scrollLeft !== undefined || scrollTop !== undefined) {
602
this.scrollableElement.setScrollPosition({ scrollLeft, scrollTop });
603
}
604
}));
605
606
this.onDidScroll = this.scrollableElement.onScroll;
607
this._register(this.onDidScroll(e => {
608
if (e.scrollTopChanged) {
609
this.viewContainer.scrollTop = e.scrollTop;
610
}
611
612
if (e.scrollLeftChanged) {
613
this.viewContainer.scrollLeft = e.scrollLeft;
614
}
615
}));
616
617
append(this.el, this.scrollableElement.getDomNode());
618
619
this.style(options.styles || defaultStyles);
620
621
// We have an existing set of view, add them now
622
if (options.descriptor) {
623
this.size = options.descriptor.size;
624
options.descriptor.views.forEach((viewDescriptor, index) => {
625
const sizing = types.isUndefined(viewDescriptor.visible) || viewDescriptor.visible ? viewDescriptor.size : { type: 'invisible', cachedVisibleSize: viewDescriptor.size } satisfies InvisibleSizing;
626
627
const view = viewDescriptor.view;
628
this.doAddView(view, sizing, index, true);
629
});
630
631
// Initialize content size and proportions for first layout
632
this._contentSize = this.viewItems.reduce((r, i) => r + i.size, 0);
633
this.saveProportions();
634
}
635
}
636
637
style(styles: ISplitViewStyles): void {
638
if (styles.separatorBorder.isTransparent()) {
639
this.el.classList.remove('separator-border');
640
this.el.style.removeProperty('--separator-border');
641
} else {
642
this.el.classList.add('separator-border');
643
this.el.style.setProperty('--separator-border', styles.separatorBorder.toString());
644
}
645
}
646
647
/**
648
* Add a {@link IView view} to this {@link SplitView}.
649
*
650
* @param view The view to add.
651
* @param size Either a fixed size, or a dynamic {@link Sizing} strategy.
652
* @param index The index to insert the view on.
653
* @param skipLayout Whether layout should be skipped.
654
*/
655
addView(view: TView, size: number | Sizing, index = this.viewItems.length, skipLayout?: boolean): void {
656
this.doAddView(view, size, index, skipLayout);
657
}
658
659
/**
660
* Remove a {@link IView view} from this {@link SplitView}.
661
*
662
* @param index The index where the {@link IView view} is located.
663
* @param sizing Whether to distribute other {@link IView view}'s sizes.
664
*/
665
removeView(index: number, sizing?: Sizing): TView {
666
if (index < 0 || index >= this.viewItems.length) {
667
throw new Error('Index out of bounds');
668
}
669
670
if (this.state !== State.Idle) {
671
throw new Error('Cant modify splitview');
672
}
673
674
this.state = State.Busy;
675
676
try {
677
if (sizing?.type === 'auto') {
678
if (this.areViewsDistributed()) {
679
sizing = { type: 'distribute' };
680
} else {
681
sizing = { type: 'split', index: sizing.index };
682
}
683
}
684
685
// Save referene view, in case of `split` sizing
686
const referenceViewItem = sizing?.type === 'split' ? this.viewItems[sizing.index] : undefined;
687
688
// Remove view
689
const viewItemToRemove = this.viewItems.splice(index, 1)[0];
690
691
// Resize reference view, in case of `split` sizing
692
if (referenceViewItem) {
693
referenceViewItem.size += viewItemToRemove.size;
694
}
695
696
// Remove sash
697
if (this.viewItems.length >= 1) {
698
const sashIndex = Math.max(index - 1, 0);
699
const sashItem = this.sashItems.splice(sashIndex, 1)[0];
700
sashItem.disposable.dispose();
701
}
702
703
this.relayout();
704
705
if (sizing?.type === 'distribute') {
706
this.distributeViewSizes();
707
}
708
709
const result = viewItemToRemove.view;
710
viewItemToRemove.dispose();
711
return result;
712
713
} finally {
714
this.state = State.Idle;
715
}
716
}
717
718
removeAllViews(): TView[] {
719
if (this.state !== State.Idle) {
720
throw new Error('Cant modify splitview');
721
}
722
723
this.state = State.Busy;
724
725
try {
726
const viewItems = this.viewItems.splice(0, this.viewItems.length);
727
728
for (const viewItem of viewItems) {
729
viewItem.dispose();
730
}
731
732
const sashItems = this.sashItems.splice(0, this.sashItems.length);
733
734
for (const sashItem of sashItems) {
735
sashItem.disposable.dispose();
736
}
737
738
this.relayout();
739
return viewItems.map(i => i.view);
740
741
} finally {
742
this.state = State.Idle;
743
}
744
}
745
746
/**
747
* Move a {@link IView view} to a different index.
748
*
749
* @param from The source index.
750
* @param to The target index.
751
*/
752
moveView(from: number, to: number): void {
753
if (this.state !== State.Idle) {
754
throw new Error('Cant modify splitview');
755
}
756
757
const cachedVisibleSize = this.getViewCachedVisibleSize(from);
758
const sizing = typeof cachedVisibleSize === 'undefined' ? this.getViewSize(from) : Sizing.Invisible(cachedVisibleSize);
759
const view = this.removeView(from);
760
this.addView(view, sizing, to);
761
}
762
763
764
/**
765
* Swap two {@link IView views}.
766
*
767
* @param from The source index.
768
* @param to The target index.
769
*/
770
swapViews(from: number, to: number): void {
771
if (this.state !== State.Idle) {
772
throw new Error('Cant modify splitview');
773
}
774
775
if (from > to) {
776
return this.swapViews(to, from);
777
}
778
779
const fromSize = this.getViewSize(from);
780
const toSize = this.getViewSize(to);
781
const toView = this.removeView(to);
782
const fromView = this.removeView(from);
783
784
this.addView(toView, fromSize, from);
785
this.addView(fromView, toSize, to);
786
}
787
788
/**
789
* Returns whether the {@link IView view} is visible.
790
*
791
* @param index The {@link IView view} index.
792
*/
793
isViewVisible(index: number): boolean {
794
if (index < 0 || index >= this.viewItems.length) {
795
throw new Error('Index out of bounds');
796
}
797
798
const viewItem = this.viewItems[index];
799
return viewItem.visible;
800
}
801
802
/**
803
* Set a {@link IView view}'s visibility.
804
*
805
* @param index The {@link IView view} index.
806
* @param visible Whether the {@link IView view} should be visible.
807
*/
808
setViewVisible(index: number, visible: boolean): void {
809
if (index < 0 || index >= this.viewItems.length) {
810
throw new Error('Index out of bounds');
811
}
812
813
const viewItem = this.viewItems[index];
814
viewItem.setVisible(visible);
815
816
this.distributeEmptySpace(index);
817
this.layoutViews();
818
this.saveProportions();
819
}
820
821
/**
822
* Returns the {@link IView view}'s size previously to being hidden.
823
*
824
* @param index The {@link IView view} index.
825
*/
826
getViewCachedVisibleSize(index: number): number | undefined {
827
if (index < 0 || index >= this.viewItems.length) {
828
throw new Error('Index out of bounds');
829
}
830
831
const viewItem = this.viewItems[index];
832
return viewItem.cachedVisibleSize;
833
}
834
835
/**
836
* Layout the {@link SplitView}.
837
*
838
* @param size The entire size of the {@link SplitView}.
839
* @param layoutContext An optional layout context to pass along to {@link IView views}.
840
*/
841
layout(size: number, layoutContext?: TLayoutContext): void {
842
const previousSize = Math.max(this.size, this._contentSize);
843
this.size = size;
844
this.layoutContext = layoutContext;
845
846
if (!this.proportions) {
847
const indexes = range(this.viewItems.length);
848
const lowPriorityIndexes = indexes.filter(i => this.viewItems[i].priority === LayoutPriority.Low);
849
const highPriorityIndexes = indexes.filter(i => this.viewItems[i].priority === LayoutPriority.High);
850
851
this.resize(this.viewItems.length - 1, size - previousSize, undefined, lowPriorityIndexes, highPriorityIndexes);
852
} else {
853
let total = 0;
854
855
for (let i = 0; i < this.viewItems.length; i++) {
856
const item = this.viewItems[i];
857
const proportion = this.proportions[i];
858
859
if (typeof proportion === 'number') {
860
total += proportion;
861
} else {
862
size -= item.size;
863
}
864
}
865
866
for (let i = 0; i < this.viewItems.length; i++) {
867
const item = this.viewItems[i];
868
const proportion = this.proportions[i];
869
870
if (typeof proportion === 'number' && total > 0) {
871
item.size = clamp(Math.round(proportion * size / total), item.minimumSize, item.maximumSize);
872
}
873
}
874
}
875
876
this.distributeEmptySpace();
877
this.layoutViews();
878
}
879
880
private saveProportions(): void {
881
if (this.proportionalLayout && this._contentSize > 0) {
882
this.proportions = this.viewItems.map(v => v.proportionalLayout && v.visible ? v.size / this._contentSize : undefined);
883
}
884
}
885
886
private onSashStart({ sash, start, alt }: ISashEvent): void {
887
for (const item of this.viewItems) {
888
item.enabled = false;
889
}
890
891
const index = this.sashItems.findIndex(item => item.sash === sash);
892
893
// This way, we can press Alt while we resize a sash, macOS style!
894
const disposable = combinedDisposable(
895
addDisposableListener(this.el.ownerDocument.body, 'keydown', e => resetSashDragState(this.sashDragState!.current, e.altKey)),
896
addDisposableListener(this.el.ownerDocument.body, 'keyup', () => resetSashDragState(this.sashDragState!.current, false))
897
);
898
899
const resetSashDragState = (start: number, alt: boolean) => {
900
const sizes = this.viewItems.map(i => i.size);
901
let minDelta = Number.NEGATIVE_INFINITY;
902
let maxDelta = Number.POSITIVE_INFINITY;
903
904
if (this.inverseAltBehavior) {
905
alt = !alt;
906
}
907
908
if (alt) {
909
// When we're using the last sash with Alt, we're resizing
910
// the view to the left/up, instead of right/down as usual
911
// Thus, we must do the inverse of the usual
912
const isLastSash = index === this.sashItems.length - 1;
913
914
if (isLastSash) {
915
const viewItem = this.viewItems[index];
916
minDelta = (viewItem.minimumSize - viewItem.size) / 2;
917
maxDelta = (viewItem.maximumSize - viewItem.size) / 2;
918
} else {
919
const viewItem = this.viewItems[index + 1];
920
minDelta = (viewItem.size - viewItem.maximumSize) / 2;
921
maxDelta = (viewItem.size - viewItem.minimumSize) / 2;
922
}
923
}
924
925
let snapBefore: ISashDragSnapState | undefined;
926
let snapAfter: ISashDragSnapState | undefined;
927
928
if (!alt) {
929
const upIndexes = range(index, -1);
930
const downIndexes = range(index + 1, this.viewItems.length);
931
const minDeltaUp = upIndexes.reduce((r, i) => r + (this.viewItems[i].minimumSize - sizes[i]), 0);
932
const maxDeltaUp = upIndexes.reduce((r, i) => r + (this.viewItems[i].viewMaximumSize - sizes[i]), 0);
933
const maxDeltaDown = downIndexes.length === 0 ? Number.POSITIVE_INFINITY : downIndexes.reduce((r, i) => r + (sizes[i] - this.viewItems[i].minimumSize), 0);
934
const minDeltaDown = downIndexes.length === 0 ? Number.NEGATIVE_INFINITY : downIndexes.reduce((r, i) => r + (sizes[i] - this.viewItems[i].viewMaximumSize), 0);
935
const minDelta = Math.max(minDeltaUp, minDeltaDown);
936
const maxDelta = Math.min(maxDeltaDown, maxDeltaUp);
937
const snapBeforeIndex = this.findFirstSnapIndex(upIndexes);
938
const snapAfterIndex = this.findFirstSnapIndex(downIndexes);
939
940
if (typeof snapBeforeIndex === 'number') {
941
const viewItem = this.viewItems[snapBeforeIndex];
942
const halfSize = Math.floor(viewItem.viewMinimumSize / 2);
943
944
snapBefore = {
945
index: snapBeforeIndex,
946
limitDelta: viewItem.visible ? minDelta - halfSize : minDelta + halfSize,
947
size: viewItem.size
948
};
949
}
950
951
if (typeof snapAfterIndex === 'number') {
952
const viewItem = this.viewItems[snapAfterIndex];
953
const halfSize = Math.floor(viewItem.viewMinimumSize / 2);
954
955
snapAfter = {
956
index: snapAfterIndex,
957
limitDelta: viewItem.visible ? maxDelta + halfSize : maxDelta - halfSize,
958
size: viewItem.size
959
};
960
}
961
}
962
963
this.sashDragState = { start, current: start, index, sizes, minDelta, maxDelta, alt, snapBefore, snapAfter, disposable };
964
};
965
966
resetSashDragState(start, alt);
967
}
968
969
private onSashChange({ current }: ISashEvent): void {
970
const { index, start, sizes, alt, minDelta, maxDelta, snapBefore, snapAfter } = this.sashDragState!;
971
this.sashDragState!.current = current;
972
973
const delta = current - start;
974
const newDelta = this.resize(index, delta, sizes, undefined, undefined, minDelta, maxDelta, snapBefore, snapAfter);
975
976
if (alt) {
977
const isLastSash = index === this.sashItems.length - 1;
978
const newSizes = this.viewItems.map(i => i.size);
979
const viewItemIndex = isLastSash ? index : index + 1;
980
const viewItem = this.viewItems[viewItemIndex];
981
const newMinDelta = viewItem.size - viewItem.maximumSize;
982
const newMaxDelta = viewItem.size - viewItem.minimumSize;
983
const resizeIndex = isLastSash ? index - 1 : index + 1;
984
985
this.resize(resizeIndex, -newDelta, newSizes, undefined, undefined, newMinDelta, newMaxDelta);
986
}
987
988
this.distributeEmptySpace();
989
this.layoutViews();
990
}
991
992
private onSashEnd(index: number): void {
993
this._onDidSashChange.fire(index);
994
this.sashDragState!.disposable.dispose();
995
this.saveProportions();
996
997
for (const item of this.viewItems) {
998
item.enabled = true;
999
}
1000
}
1001
1002
private onViewChange(item: ViewItem<TLayoutContext, TView>, size: number | undefined): void {
1003
const index = this.viewItems.indexOf(item);
1004
1005
if (index < 0 || index >= this.viewItems.length) {
1006
return;
1007
}
1008
1009
size = typeof size === 'number' ? size : item.size;
1010
size = clamp(size, item.minimumSize, item.maximumSize);
1011
1012
if (this.inverseAltBehavior && index > 0) {
1013
// In this case, we want the view to grow or shrink both sides equally
1014
// so we just resize the "left" side by half and let `resize` do the clamping magic
1015
this.resize(index - 1, Math.floor((item.size - size) / 2));
1016
this.distributeEmptySpace();
1017
this.layoutViews();
1018
} else {
1019
item.size = size;
1020
this.relayout([index], undefined);
1021
}
1022
}
1023
1024
/**
1025
* Resize a {@link IView view} within the {@link SplitView}.
1026
*
1027
* @param index The {@link IView view} index.
1028
* @param size The {@link IView view} size.
1029
*/
1030
resizeView(index: number, size: number): void {
1031
if (index < 0 || index >= this.viewItems.length) {
1032
return;
1033
}
1034
1035
if (this.state !== State.Idle) {
1036
throw new Error('Cant modify splitview');
1037
}
1038
1039
this.state = State.Busy;
1040
1041
try {
1042
const indexes = range(this.viewItems.length).filter(i => i !== index);
1043
const lowPriorityIndexes = [...indexes.filter(i => this.viewItems[i].priority === LayoutPriority.Low), index];
1044
const highPriorityIndexes = indexes.filter(i => this.viewItems[i].priority === LayoutPriority.High);
1045
1046
const item = this.viewItems[index];
1047
size = Math.round(size);
1048
size = clamp(size, item.minimumSize, Math.min(item.maximumSize, this.size));
1049
1050
item.size = size;
1051
this.relayout(lowPriorityIndexes, highPriorityIndexes);
1052
} finally {
1053
this.state = State.Idle;
1054
}
1055
}
1056
1057
/**
1058
* Returns whether all other {@link IView views} are at their minimum size.
1059
*/
1060
isViewExpanded(index: number): boolean {
1061
if (index < 0 || index >= this.viewItems.length) {
1062
return false;
1063
}
1064
1065
for (const item of this.viewItems) {
1066
if (item !== this.viewItems[index] && item.size > item.minimumSize) {
1067
return false;
1068
}
1069
}
1070
1071
return true;
1072
}
1073
1074
/**
1075
* Distribute the entire {@link SplitView} size among all {@link IView views}.
1076
*/
1077
distributeViewSizes(): void {
1078
const flexibleViewItems: ViewItem<TLayoutContext, TView>[] = [];
1079
let flexibleSize = 0;
1080
1081
for (const item of this.viewItems) {
1082
if (item.maximumSize - item.minimumSize > 0) {
1083
flexibleViewItems.push(item);
1084
flexibleSize += item.size;
1085
}
1086
}
1087
1088
const size = Math.floor(flexibleSize / flexibleViewItems.length);
1089
1090
for (const item of flexibleViewItems) {
1091
item.size = clamp(size, item.minimumSize, item.maximumSize);
1092
}
1093
1094
const indexes = range(this.viewItems.length);
1095
const lowPriorityIndexes = indexes.filter(i => this.viewItems[i].priority === LayoutPriority.Low);
1096
const highPriorityIndexes = indexes.filter(i => this.viewItems[i].priority === LayoutPriority.High);
1097
1098
this.relayout(lowPriorityIndexes, highPriorityIndexes);
1099
}
1100
1101
/**
1102
* Returns the size of a {@link IView view}.
1103
*/
1104
getViewSize(index: number): number {
1105
if (index < 0 || index >= this.viewItems.length) {
1106
return -1;
1107
}
1108
1109
return this.viewItems[index].size;
1110
}
1111
1112
private doAddView(view: TView, size: number | Sizing, index = this.viewItems.length, skipLayout?: boolean): void {
1113
if (this.state !== State.Idle) {
1114
throw new Error('Cant modify splitview');
1115
}
1116
1117
this.state = State.Busy;
1118
1119
try {
1120
// Add view
1121
const container = $('.split-view-view');
1122
1123
if (index === this.viewItems.length) {
1124
this.viewContainer.appendChild(container);
1125
} else {
1126
this.viewContainer.insertBefore(container, this.viewContainer.children.item(index));
1127
}
1128
1129
const onChangeDisposable = view.onDidChange(size => this.onViewChange(item, size));
1130
const containerDisposable = toDisposable(() => container.remove());
1131
const disposable = combinedDisposable(onChangeDisposable, containerDisposable);
1132
1133
let viewSize: ViewItemSize;
1134
1135
if (typeof size === 'number') {
1136
viewSize = size;
1137
} else {
1138
if (size.type === 'auto') {
1139
if (this.areViewsDistributed()) {
1140
size = { type: 'distribute' };
1141
} else {
1142
size = { type: 'split', index: size.index };
1143
}
1144
}
1145
1146
if (size.type === 'split') {
1147
viewSize = this.getViewSize(size.index) / 2;
1148
} else if (size.type === 'invisible') {
1149
viewSize = { cachedVisibleSize: size.cachedVisibleSize };
1150
} else {
1151
viewSize = view.minimumSize;
1152
}
1153
}
1154
1155
const item = this.orientation === Orientation.VERTICAL
1156
? new VerticalViewItem(container, view, viewSize, disposable)
1157
: new HorizontalViewItem(container, view, viewSize, disposable);
1158
1159
this.viewItems.splice(index, 0, item);
1160
1161
// Add sash
1162
if (this.viewItems.length > 1) {
1163
const opts = { orthogonalStartSash: this.orthogonalStartSash, orthogonalEndSash: this.orthogonalEndSash };
1164
1165
const sash = this.orientation === Orientation.VERTICAL
1166
? new Sash(this.sashContainer, { getHorizontalSashTop: s => this.getSashPosition(s), getHorizontalSashWidth: this.getSashOrthogonalSize }, { ...opts, orientation: Orientation.HORIZONTAL })
1167
: new Sash(this.sashContainer, { getVerticalSashLeft: s => this.getSashPosition(s), getVerticalSashHeight: this.getSashOrthogonalSize }, { ...opts, orientation: Orientation.VERTICAL });
1168
1169
const sashEventMapper = this.orientation === Orientation.VERTICAL
1170
? (e: IBaseSashEvent) => ({ sash, start: e.startY, current: e.currentY, alt: e.altKey })
1171
: (e: IBaseSashEvent) => ({ sash, start: e.startX, current: e.currentX, alt: e.altKey });
1172
1173
const onStart = Event.map(sash.onDidStart, sashEventMapper);
1174
const onStartDisposable = onStart(this.onSashStart, this);
1175
const onChange = Event.map(sash.onDidChange, sashEventMapper);
1176
const onChangeDisposable = onChange(this.onSashChange, this);
1177
const onEnd = Event.map(sash.onDidEnd, () => this.sashItems.findIndex(item => item.sash === sash));
1178
const onEndDisposable = onEnd(this.onSashEnd, this);
1179
1180
const onDidResetDisposable = sash.onDidReset(() => {
1181
const index = this.sashItems.findIndex(item => item.sash === sash);
1182
const upIndexes = range(index, -1);
1183
const downIndexes = range(index + 1, this.viewItems.length);
1184
const snapBeforeIndex = this.findFirstSnapIndex(upIndexes);
1185
const snapAfterIndex = this.findFirstSnapIndex(downIndexes);
1186
1187
if (typeof snapBeforeIndex === 'number' && !this.viewItems[snapBeforeIndex].visible) {
1188
return;
1189
}
1190
1191
if (typeof snapAfterIndex === 'number' && !this.viewItems[snapAfterIndex].visible) {
1192
return;
1193
}
1194
1195
this._onDidSashReset.fire(index);
1196
});
1197
1198
const disposable = combinedDisposable(onStartDisposable, onChangeDisposable, onEndDisposable, onDidResetDisposable, sash);
1199
const sashItem: ISashItem = { sash, disposable };
1200
1201
this.sashItems.splice(index - 1, 0, sashItem);
1202
}
1203
1204
container.appendChild(view.element);
1205
1206
let highPriorityIndexes: number[] | undefined;
1207
1208
if (typeof size !== 'number' && size.type === 'split') {
1209
highPriorityIndexes = [size.index];
1210
}
1211
1212
if (!skipLayout) {
1213
this.relayout([index], highPriorityIndexes);
1214
}
1215
1216
1217
if (!skipLayout && typeof size !== 'number' && size.type === 'distribute') {
1218
this.distributeViewSizes();
1219
}
1220
1221
} finally {
1222
this.state = State.Idle;
1223
}
1224
}
1225
1226
private relayout(lowPriorityIndexes?: number[], highPriorityIndexes?: number[]): void {
1227
const contentSize = this.viewItems.reduce((r, i) => r + i.size, 0);
1228
1229
this.resize(this.viewItems.length - 1, this.size - contentSize, undefined, lowPriorityIndexes, highPriorityIndexes);
1230
this.distributeEmptySpace();
1231
this.layoutViews();
1232
this.saveProportions();
1233
}
1234
1235
private resize(
1236
index: number,
1237
delta: number,
1238
sizes = this.viewItems.map(i => i.size),
1239
lowPriorityIndexes?: number[],
1240
highPriorityIndexes?: number[],
1241
overloadMinDelta: number = Number.NEGATIVE_INFINITY,
1242
overloadMaxDelta: number = Number.POSITIVE_INFINITY,
1243
snapBefore?: ISashDragSnapState,
1244
snapAfter?: ISashDragSnapState
1245
): number {
1246
if (index < 0 || index >= this.viewItems.length) {
1247
return 0;
1248
}
1249
1250
const upIndexes = range(index, -1);
1251
const downIndexes = range(index + 1, this.viewItems.length);
1252
1253
if (highPriorityIndexes) {
1254
for (const index of highPriorityIndexes) {
1255
pushToStart(upIndexes, index);
1256
pushToStart(downIndexes, index);
1257
}
1258
}
1259
1260
if (lowPriorityIndexes) {
1261
for (const index of lowPriorityIndexes) {
1262
pushToEnd(upIndexes, index);
1263
pushToEnd(downIndexes, index);
1264
}
1265
}
1266
1267
const upItems = upIndexes.map(i => this.viewItems[i]);
1268
const upSizes = upIndexes.map(i => sizes[i]);
1269
1270
const downItems = downIndexes.map(i => this.viewItems[i]);
1271
const downSizes = downIndexes.map(i => sizes[i]);
1272
1273
const minDeltaUp = upIndexes.reduce((r, i) => r + (this.viewItems[i].minimumSize - sizes[i]), 0);
1274
const maxDeltaUp = upIndexes.reduce((r, i) => r + (this.viewItems[i].maximumSize - sizes[i]), 0);
1275
const maxDeltaDown = downIndexes.length === 0 ? Number.POSITIVE_INFINITY : downIndexes.reduce((r, i) => r + (sizes[i] - this.viewItems[i].minimumSize), 0);
1276
const minDeltaDown = downIndexes.length === 0 ? Number.NEGATIVE_INFINITY : downIndexes.reduce((r, i) => r + (sizes[i] - this.viewItems[i].maximumSize), 0);
1277
const minDelta = Math.max(minDeltaUp, minDeltaDown, overloadMinDelta);
1278
const maxDelta = Math.min(maxDeltaDown, maxDeltaUp, overloadMaxDelta);
1279
1280
let snapped = false;
1281
1282
if (snapBefore) {
1283
const snapView = this.viewItems[snapBefore.index];
1284
const visible = delta >= snapBefore.limitDelta;
1285
snapped = visible !== snapView.visible;
1286
snapView.setVisible(visible, snapBefore.size);
1287
}
1288
1289
if (!snapped && snapAfter) {
1290
const snapView = this.viewItems[snapAfter.index];
1291
const visible = delta < snapAfter.limitDelta;
1292
snapped = visible !== snapView.visible;
1293
snapView.setVisible(visible, snapAfter.size);
1294
}
1295
1296
if (snapped) {
1297
return this.resize(index, delta, sizes, lowPriorityIndexes, highPriorityIndexes, overloadMinDelta, overloadMaxDelta);
1298
}
1299
1300
delta = clamp(delta, minDelta, maxDelta);
1301
1302
for (let i = 0, deltaUp = delta; i < upItems.length; i++) {
1303
const item = upItems[i];
1304
const size = clamp(upSizes[i] + deltaUp, item.minimumSize, item.maximumSize);
1305
const viewDelta = size - upSizes[i];
1306
1307
deltaUp -= viewDelta;
1308
item.size = size;
1309
}
1310
1311
for (let i = 0, deltaDown = delta; i < downItems.length; i++) {
1312
const item = downItems[i];
1313
const size = clamp(downSizes[i] - deltaDown, item.minimumSize, item.maximumSize);
1314
const viewDelta = size - downSizes[i];
1315
1316
deltaDown += viewDelta;
1317
item.size = size;
1318
}
1319
1320
return delta;
1321
}
1322
1323
private distributeEmptySpace(lowPriorityIndex?: number): void {
1324
const contentSize = this.viewItems.reduce((r, i) => r + i.size, 0);
1325
let emptyDelta = this.size - contentSize;
1326
1327
const indexes = range(this.viewItems.length - 1, -1);
1328
const lowPriorityIndexes = indexes.filter(i => this.viewItems[i].priority === LayoutPriority.Low);
1329
const highPriorityIndexes = indexes.filter(i => this.viewItems[i].priority === LayoutPriority.High);
1330
1331
for (const index of highPriorityIndexes) {
1332
pushToStart(indexes, index);
1333
}
1334
1335
for (const index of lowPriorityIndexes) {
1336
pushToEnd(indexes, index);
1337
}
1338
1339
if (typeof lowPriorityIndex === 'number') {
1340
pushToEnd(indexes, lowPriorityIndex);
1341
}
1342
1343
for (let i = 0; emptyDelta !== 0 && i < indexes.length; i++) {
1344
const item = this.viewItems[indexes[i]];
1345
const size = clamp(item.size + emptyDelta, item.minimumSize, item.maximumSize);
1346
const viewDelta = size - item.size;
1347
1348
emptyDelta -= viewDelta;
1349
item.size = size;
1350
}
1351
}
1352
1353
private layoutViews(): void {
1354
// Save new content size
1355
this._contentSize = this.viewItems.reduce((r, i) => r + i.size, 0);
1356
1357
// Layout views
1358
let offset = 0;
1359
1360
for (const viewItem of this.viewItems) {
1361
viewItem.layout(offset, this.layoutContext);
1362
offset += viewItem.size;
1363
}
1364
1365
// Layout sashes
1366
this.sashItems.forEach(item => item.sash.layout());
1367
this.updateSashEnablement();
1368
this.updateScrollableElement();
1369
}
1370
1371
private updateScrollableElement(): void {
1372
if (this.orientation === Orientation.VERTICAL) {
1373
this.scrollableElement.setScrollDimensions({
1374
height: this.size,
1375
scrollHeight: this._contentSize
1376
});
1377
} else {
1378
this.scrollableElement.setScrollDimensions({
1379
width: this.size,
1380
scrollWidth: this._contentSize
1381
});
1382
}
1383
}
1384
1385
private updateSashEnablement(): void {
1386
let previous = false;
1387
const collapsesDown = this.viewItems.map(i => previous = (i.size - i.minimumSize > 0) || previous);
1388
1389
previous = false;
1390
const expandsDown = this.viewItems.map(i => previous = (i.maximumSize - i.size > 0) || previous);
1391
1392
const reverseViews = [...this.viewItems].reverse();
1393
previous = false;
1394
const collapsesUp = reverseViews.map(i => previous = (i.size - i.minimumSize > 0) || previous).reverse();
1395
1396
previous = false;
1397
const expandsUp = reverseViews.map(i => previous = (i.maximumSize - i.size > 0) || previous).reverse();
1398
1399
let position = 0;
1400
for (let index = 0; index < this.sashItems.length; index++) {
1401
const { sash } = this.sashItems[index];
1402
const viewItem = this.viewItems[index];
1403
position += viewItem.size;
1404
1405
const min = !(collapsesDown[index] && expandsUp[index + 1]);
1406
const max = !(expandsDown[index] && collapsesUp[index + 1]);
1407
1408
if (min && max) {
1409
const upIndexes = range(index, -1);
1410
const downIndexes = range(index + 1, this.viewItems.length);
1411
const snapBeforeIndex = this.findFirstSnapIndex(upIndexes);
1412
const snapAfterIndex = this.findFirstSnapIndex(downIndexes);
1413
1414
const snappedBefore = typeof snapBeforeIndex === 'number' && !this.viewItems[snapBeforeIndex].visible;
1415
const snappedAfter = typeof snapAfterIndex === 'number' && !this.viewItems[snapAfterIndex].visible;
1416
1417
if (snappedBefore && collapsesUp[index] && (position > 0 || this.startSnappingEnabled)) {
1418
sash.state = SashState.AtMinimum;
1419
} else if (snappedAfter && collapsesDown[index] && (position < this._contentSize || this.endSnappingEnabled)) {
1420
sash.state = SashState.AtMaximum;
1421
} else {
1422
sash.state = SashState.Disabled;
1423
}
1424
} else if (min && !max) {
1425
sash.state = SashState.AtMinimum;
1426
} else if (!min && max) {
1427
sash.state = SashState.AtMaximum;
1428
} else {
1429
sash.state = SashState.Enabled;
1430
}
1431
}
1432
}
1433
1434
private getSashPosition(sash: Sash): number {
1435
let position = 0;
1436
1437
for (let i = 0; i < this.sashItems.length; i++) {
1438
position += this.viewItems[i].size;
1439
1440
if (this.sashItems[i].sash === sash) {
1441
return position;
1442
}
1443
}
1444
1445
return 0;
1446
}
1447
1448
private findFirstSnapIndex(indexes: number[]): number | undefined {
1449
// visible views first
1450
for (const index of indexes) {
1451
const viewItem = this.viewItems[index];
1452
1453
if (!viewItem.visible) {
1454
continue;
1455
}
1456
1457
if (viewItem.snap) {
1458
return index;
1459
}
1460
}
1461
1462
// then, hidden views
1463
for (const index of indexes) {
1464
const viewItem = this.viewItems[index];
1465
1466
if (viewItem.visible && viewItem.maximumSize - viewItem.minimumSize > 0) {
1467
return undefined;
1468
}
1469
1470
if (!viewItem.visible && viewItem.snap) {
1471
return index;
1472
}
1473
}
1474
1475
return undefined;
1476
}
1477
1478
private areViewsDistributed() {
1479
let min = undefined, max = undefined;
1480
1481
for (const view of this.viewItems) {
1482
min = min === undefined ? view.size : Math.min(min, view.size);
1483
max = max === undefined ? view.size : Math.max(max, view.size);
1484
1485
if (max - min > 2) {
1486
return false;
1487
}
1488
}
1489
1490
return true;
1491
}
1492
1493
override dispose(): void {
1494
this.sashDragState?.disposable.dispose();
1495
1496
dispose(this.viewItems);
1497
this.viewItems = [];
1498
1499
this.sashItems.forEach(i => i.disposable.dispose());
1500
this.sashItems = [];
1501
1502
super.dispose();
1503
}
1504
}
1505
1506