Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/base/browser/ui/sash/sash.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, EventHelper, EventLike, getWindow, isHTMLElement } from '../../dom.js';
7
import { createStyleSheet } from '../../domStylesheets.js';
8
import { DomEmitter } from '../../event.js';
9
import { EventType, Gesture } from '../../touch.js';
10
import { Delayer } from '../../../common/async.js';
11
import { memoize } from '../../../common/decorators.js';
12
import { Emitter, Event } from '../../../common/event.js';
13
import { Disposable, DisposableStore, toDisposable } from '../../../common/lifecycle.js';
14
import { isMacintosh } from '../../../common/platform.js';
15
import './sash.css';
16
17
/**
18
* Allow the sashes to be visible at runtime.
19
* @remark Use for development purposes only.
20
*/
21
const DEBUG = false;
22
// DEBUG = Boolean("true"); // done "weirdly" so that a lint warning prevents you from pushing this
23
24
/**
25
* A vertical sash layout provider provides position and height for a sash.
26
*/
27
export interface IVerticalSashLayoutProvider {
28
getVerticalSashLeft(sash: Sash): number;
29
getVerticalSashTop?(sash: Sash): number;
30
getVerticalSashHeight?(sash: Sash): number;
31
}
32
33
/**
34
* A vertical sash layout provider provides position and width for a sash.
35
*/
36
export interface IHorizontalSashLayoutProvider {
37
getHorizontalSashTop(sash: Sash): number;
38
getHorizontalSashLeft?(sash: Sash): number;
39
getHorizontalSashWidth?(sash: Sash): number;
40
}
41
42
type ISashLayoutProvider = IVerticalSashLayoutProvider | IHorizontalSashLayoutProvider;
43
44
export interface ISashEvent {
45
readonly startX: number;
46
readonly currentX: number;
47
readonly startY: number;
48
readonly currentY: number;
49
readonly altKey: boolean;
50
}
51
52
export enum OrthogonalEdge {
53
North = 'north',
54
South = 'south',
55
East = 'east',
56
West = 'west'
57
}
58
59
export interface IBoundarySashes {
60
readonly top?: Sash;
61
readonly right?: Sash;
62
readonly bottom?: Sash;
63
readonly left?: Sash;
64
}
65
66
export interface ISashOptions {
67
68
/**
69
* Whether a sash is horizontal or vertical.
70
*/
71
readonly orientation: Orientation;
72
73
/**
74
* The width or height of a vertical or horizontal sash, respectively.
75
*/
76
readonly size?: number;
77
78
/**
79
* A reference to another sash, perpendicular to this one, which
80
* aligns at the start of this one. A corner sash will be created
81
* automatically at that location.
82
*
83
* The start of a horizontal sash is its left-most position.
84
* The start of a vertical sash is its top-most position.
85
*/
86
readonly orthogonalStartSash?: Sash;
87
88
/**
89
* A reference to another sash, perpendicular to this one, which
90
* aligns at the end of this one. A corner sash will be created
91
* automatically at that location.
92
*
93
* The end of a horizontal sash is its right-most position.
94
* The end of a vertical sash is its bottom-most position.
95
*/
96
readonly orthogonalEndSash?: Sash;
97
98
/**
99
* Provides a hint as to what mouse cursor to use whenever the user
100
* hovers over a corner sash provided by this and an orthogonal sash.
101
*/
102
readonly orthogonalEdge?: OrthogonalEdge;
103
}
104
105
export interface IVerticalSashOptions extends ISashOptions {
106
readonly orientation: Orientation.VERTICAL;
107
}
108
109
export interface IHorizontalSashOptions extends ISashOptions {
110
readonly orientation: Orientation.HORIZONTAL;
111
}
112
113
export const enum Orientation {
114
VERTICAL,
115
HORIZONTAL
116
}
117
118
export const enum SashState {
119
120
/**
121
* Disable any UI interaction.
122
*/
123
Disabled,
124
125
/**
126
* Allow dragging down or to the right, depending on the sash orientation.
127
*
128
* Some OSs allow customizing the mouse cursor differently whenever
129
* some resizable component can't be any smaller, but can be larger.
130
*/
131
AtMinimum,
132
133
/**
134
* Allow dragging up or to the left, depending on the sash orientation.
135
*
136
* Some OSs allow customizing the mouse cursor differently whenever
137
* some resizable component can't be any larger, but can be smaller.
138
*/
139
AtMaximum,
140
141
/**
142
* Enable dragging.
143
*/
144
Enabled
145
}
146
147
let globalSize = 4;
148
const onDidChangeGlobalSize = new Emitter<number>();
149
export function setGlobalSashSize(size: number): void {
150
globalSize = size;
151
onDidChangeGlobalSize.fire(size);
152
}
153
154
let globalHoverDelay = 300;
155
const onDidChangeHoverDelay = new Emitter<number>();
156
export function setGlobalHoverDelay(size: number): void {
157
globalHoverDelay = size;
158
onDidChangeHoverDelay.fire(size);
159
}
160
161
interface PointerEvent extends EventLike {
162
readonly pageX: number;
163
readonly pageY: number;
164
readonly altKey: boolean;
165
readonly target: EventTarget | null;
166
readonly initialTarget?: EventTarget | undefined;
167
}
168
169
interface IPointerEventFactory {
170
readonly onPointerMove: Event<PointerEvent>;
171
readonly onPointerUp: Event<PointerEvent>;
172
dispose(): void;
173
}
174
175
class MouseEventFactory implements IPointerEventFactory {
176
177
private readonly disposables = new DisposableStore();
178
179
constructor(private el: HTMLElement) { }
180
181
@memoize
182
get onPointerMove(): Event<PointerEvent> {
183
return this.disposables.add(new DomEmitter(getWindow(this.el), 'mousemove')).event;
184
}
185
186
@memoize
187
get onPointerUp(): Event<PointerEvent> {
188
return this.disposables.add(new DomEmitter(getWindow(this.el), 'mouseup')).event;
189
}
190
191
dispose(): void {
192
this.disposables.dispose();
193
}
194
}
195
196
class GestureEventFactory implements IPointerEventFactory {
197
198
private readonly disposables = new DisposableStore();
199
200
@memoize
201
get onPointerMove(): Event<PointerEvent> {
202
return this.disposables.add(new DomEmitter(this.el, EventType.Change)).event;
203
}
204
205
@memoize
206
get onPointerUp(): Event<PointerEvent> {
207
return this.disposables.add(new DomEmitter(this.el, EventType.End)).event;
208
}
209
210
constructor(private el: HTMLElement) { }
211
212
dispose(): void {
213
this.disposables.dispose();
214
}
215
}
216
217
class OrthogonalPointerEventFactory implements IPointerEventFactory {
218
219
@memoize
220
get onPointerMove(): Event<PointerEvent> {
221
return this.factory.onPointerMove;
222
}
223
224
@memoize
225
get onPointerUp(): Event<PointerEvent> {
226
return this.factory.onPointerUp;
227
}
228
229
constructor(private factory: IPointerEventFactory) { }
230
231
dispose(): void {
232
// noop
233
}
234
}
235
236
const PointerEventsDisabledCssClass = 'pointer-events-disabled';
237
238
/**
239
* The {@link Sash} is the UI component which allows the user to resize other
240
* components. It's usually an invisible horizontal or vertical line which, when
241
* hovered, becomes highlighted and can be dragged along the perpendicular dimension
242
* to its direction.
243
*
244
* Features:
245
* - Touch event handling
246
* - Corner sash support
247
* - Hover with different mouse cursor support
248
* - Configurable hover size
249
* - Linked sash support, for 2x2 corner sashes
250
*/
251
export class Sash extends Disposable {
252
253
private el: HTMLElement;
254
private layoutProvider: ISashLayoutProvider;
255
private orientation: Orientation;
256
private size: number;
257
private hoverDelay = globalHoverDelay;
258
private hoverDelayer = this._register(new Delayer(this.hoverDelay));
259
260
private _state: SashState = SashState.Enabled;
261
private readonly onDidEnablementChange = this._register(new Emitter<SashState>());
262
private readonly _onDidStart = this._register(new Emitter<ISashEvent>());
263
private readonly _onDidChange = this._register(new Emitter<ISashEvent>());
264
private readonly _onDidReset = this._register(new Emitter<void>());
265
private readonly _onDidEnd = this._register(new Emitter<void>());
266
private readonly orthogonalStartSashDisposables = this._register(new DisposableStore());
267
private _orthogonalStartSash: Sash | undefined;
268
private readonly orthogonalStartDragHandleDisposables = this._register(new DisposableStore());
269
private _orthogonalStartDragHandle: HTMLElement | undefined;
270
private readonly orthogonalEndSashDisposables = this._register(new DisposableStore());
271
private _orthogonalEndSash: Sash | undefined;
272
private readonly orthogonalEndDragHandleDisposables = this._register(new DisposableStore());
273
private _orthogonalEndDragHandle: HTMLElement | undefined;
274
275
get state(): SashState { return this._state; }
276
get orthogonalStartSash(): Sash | undefined { return this._orthogonalStartSash; }
277
get orthogonalEndSash(): Sash | undefined { return this._orthogonalEndSash; }
278
279
/**
280
* The state of a sash defines whether it can be interacted with by the user
281
* as well as what mouse cursor to use, when hovered.
282
*/
283
set state(state: SashState) {
284
if (this._state === state) {
285
return;
286
}
287
288
this.el.classList.toggle('disabled', state === SashState.Disabled);
289
this.el.classList.toggle('minimum', state === SashState.AtMinimum);
290
this.el.classList.toggle('maximum', state === SashState.AtMaximum);
291
292
this._state = state;
293
this.onDidEnablementChange.fire(state);
294
}
295
296
/**
297
* An event which fires whenever the user starts dragging this sash.
298
*/
299
get onDidStart() { return this._onDidStart.event; }
300
301
/**
302
* An event which fires whenever the user moves the mouse while
303
* dragging this sash.
304
*/
305
get onDidChange() { return this._onDidChange.event; }
306
307
/**
308
* An event which fires whenever the user double clicks this sash.
309
*/
310
get onDidReset() { return this._onDidReset.event; }
311
312
/**
313
* An event which fires whenever the user stops dragging this sash.
314
*/
315
get onDidEnd() { return this._onDidEnd.event; }
316
317
/**
318
* A linked sash will be forwarded the same user interactions and events
319
* so it moves exactly the same way as this sash.
320
*
321
* Useful in 2x2 grids. Not meant for widespread usage.
322
*/
323
linkedSash: Sash | undefined = undefined;
324
325
/**
326
* A reference to another sash, perpendicular to this one, which
327
* aligns at the start of this one. A corner sash will be created
328
* automatically at that location.
329
*
330
* The start of a horizontal sash is its left-most position.
331
* The start of a vertical sash is its top-most position.
332
*/
333
set orthogonalStartSash(sash: Sash | undefined) {
334
if (this._orthogonalStartSash === sash) {
335
return;
336
}
337
338
this.orthogonalStartDragHandleDisposables.clear();
339
this.orthogonalStartSashDisposables.clear();
340
341
if (sash) {
342
const onChange = (state: SashState) => {
343
this.orthogonalStartDragHandleDisposables.clear();
344
345
if (state !== SashState.Disabled) {
346
this._orthogonalStartDragHandle = append(this.el, $('.orthogonal-drag-handle.start'));
347
this.orthogonalStartDragHandleDisposables.add(toDisposable(() => this._orthogonalStartDragHandle!.remove()));
348
this.orthogonalStartDragHandleDisposables.add(addDisposableListener(this._orthogonalStartDragHandle, 'mouseenter', () => Sash.onMouseEnter(sash)));
349
this.orthogonalStartDragHandleDisposables.add(addDisposableListener(this._orthogonalStartDragHandle, 'mouseleave', () => Sash.onMouseLeave(sash)));
350
}
351
};
352
353
this.orthogonalStartSashDisposables.add(sash.onDidEnablementChange.event(onChange, this));
354
onChange(sash.state);
355
}
356
357
this._orthogonalStartSash = sash;
358
}
359
360
/**
361
* A reference to another sash, perpendicular to this one, which
362
* aligns at the end of this one. A corner sash will be created
363
* automatically at that location.
364
*
365
* The end of a horizontal sash is its right-most position.
366
* The end of a vertical sash is its bottom-most position.
367
*/
368
369
set orthogonalEndSash(sash: Sash | undefined) {
370
if (this._orthogonalEndSash === sash) {
371
return;
372
}
373
374
this.orthogonalEndDragHandleDisposables.clear();
375
this.orthogonalEndSashDisposables.clear();
376
377
if (sash) {
378
const onChange = (state: SashState) => {
379
this.orthogonalEndDragHandleDisposables.clear();
380
381
if (state !== SashState.Disabled) {
382
this._orthogonalEndDragHandle = append(this.el, $('.orthogonal-drag-handle.end'));
383
this.orthogonalEndDragHandleDisposables.add(toDisposable(() => this._orthogonalEndDragHandle!.remove()));
384
this.orthogonalEndDragHandleDisposables.add(addDisposableListener(this._orthogonalEndDragHandle, 'mouseenter', () => Sash.onMouseEnter(sash)));
385
this.orthogonalEndDragHandleDisposables.add(addDisposableListener(this._orthogonalEndDragHandle, 'mouseleave', () => Sash.onMouseLeave(sash)));
386
}
387
};
388
389
this.orthogonalEndSashDisposables.add(sash.onDidEnablementChange.event(onChange, this));
390
onChange(sash.state);
391
}
392
393
this._orthogonalEndSash = sash;
394
}
395
396
/**
397
* Create a new vertical sash.
398
*
399
* @param container A DOM node to append the sash to.
400
* @param verticalLayoutProvider A vertical layout provider.
401
* @param options The options.
402
*/
403
constructor(container: HTMLElement, verticalLayoutProvider: IVerticalSashLayoutProvider, options: IVerticalSashOptions);
404
405
/**
406
* Create a new horizontal sash.
407
*
408
* @param container A DOM node to append the sash to.
409
* @param horizontalLayoutProvider A horizontal layout provider.
410
* @param options The options.
411
*/
412
constructor(container: HTMLElement, horizontalLayoutProvider: IHorizontalSashLayoutProvider, options: IHorizontalSashOptions);
413
constructor(container: HTMLElement, layoutProvider: ISashLayoutProvider, options: ISashOptions) {
414
super();
415
416
this.el = append(container, $('.monaco-sash'));
417
418
if (options.orthogonalEdge) {
419
this.el.classList.add(`orthogonal-edge-${options.orthogonalEdge}`);
420
}
421
422
if (isMacintosh) {
423
this.el.classList.add('mac');
424
}
425
426
this._register(addDisposableListener(this.el, 'mousedown', e => this.onPointerStart(e, new MouseEventFactory(container))));
427
this._register(addDisposableListener(this.el, 'dblclick', e => this.onPointerDoublePress(e)));
428
this._register(addDisposableListener(this.el, 'mouseenter', () => Sash.onMouseEnter(this)));
429
this._register(addDisposableListener(this.el, 'mouseleave', () => Sash.onMouseLeave(this)));
430
431
this._register(Gesture.addTarget(this.el));
432
433
this._register(addDisposableListener(this.el, EventType.Start, e => this.onPointerStart(e, new GestureEventFactory(this.el))));
434
435
let doubleTapTimeout: Timeout | undefined = undefined;
436
this._register(addDisposableListener(this.el, EventType.Tap, event => {
437
if (doubleTapTimeout) {
438
clearTimeout(doubleTapTimeout);
439
doubleTapTimeout = undefined;
440
this.onPointerDoublePress(event);
441
return;
442
}
443
444
clearTimeout(doubleTapTimeout);
445
doubleTapTimeout = setTimeout(() => doubleTapTimeout = undefined, 250);
446
}));
447
448
if (typeof options.size === 'number') {
449
this.size = options.size;
450
451
if (options.orientation === Orientation.VERTICAL) {
452
this.el.style.width = `${this.size}px`;
453
} else {
454
this.el.style.height = `${this.size}px`;
455
}
456
} else {
457
this.size = globalSize;
458
this._register(onDidChangeGlobalSize.event(size => {
459
this.size = size;
460
this.layout();
461
}));
462
}
463
464
this._register(onDidChangeHoverDelay.event(delay => this.hoverDelay = delay));
465
466
this.layoutProvider = layoutProvider;
467
468
this.orthogonalStartSash = options.orthogonalStartSash;
469
this.orthogonalEndSash = options.orthogonalEndSash;
470
471
this.orientation = options.orientation || Orientation.VERTICAL;
472
473
if (this.orientation === Orientation.HORIZONTAL) {
474
this.el.classList.add('horizontal');
475
this.el.classList.remove('vertical');
476
} else {
477
this.el.classList.remove('horizontal');
478
this.el.classList.add('vertical');
479
}
480
481
this.el.classList.toggle('debug', DEBUG);
482
483
this.layout();
484
}
485
486
private onPointerStart(event: PointerEvent, pointerEventFactory: IPointerEventFactory): void {
487
EventHelper.stop(event);
488
489
let isMultisashResize = false;
490
491
if (!(event as any).__orthogonalSashEvent) {
492
const orthogonalSash = this.getOrthogonalSash(event);
493
494
if (orthogonalSash) {
495
isMultisashResize = true;
496
(event as any).__orthogonalSashEvent = true;
497
orthogonalSash.onPointerStart(event, new OrthogonalPointerEventFactory(pointerEventFactory));
498
}
499
}
500
501
if (this.linkedSash && !(event as any).__linkedSashEvent) {
502
(event as any).__linkedSashEvent = true;
503
this.linkedSash.onPointerStart(event, new OrthogonalPointerEventFactory(pointerEventFactory));
504
}
505
506
if (!this.state) {
507
return;
508
}
509
510
const iframes = this.el.ownerDocument.getElementsByTagName('iframe');
511
for (const iframe of iframes) {
512
iframe.classList.add(PointerEventsDisabledCssClass); // disable mouse events on iframes as long as we drag the sash
513
}
514
515
const startX = event.pageX;
516
const startY = event.pageY;
517
const altKey = event.altKey;
518
const startEvent: ISashEvent = { startX, currentX: startX, startY, currentY: startY, altKey };
519
520
this.el.classList.add('active');
521
this._onDidStart.fire(startEvent);
522
523
// fix https://github.com/microsoft/vscode/issues/21675
524
const style = createStyleSheet(this.el);
525
const updateStyle = () => {
526
let cursor = '';
527
528
if (isMultisashResize) {
529
cursor = 'all-scroll';
530
} else if (this.orientation === Orientation.HORIZONTAL) {
531
if (this.state === SashState.AtMinimum) {
532
cursor = 's-resize';
533
} else if (this.state === SashState.AtMaximum) {
534
cursor = 'n-resize';
535
} else {
536
cursor = isMacintosh ? 'row-resize' : 'ns-resize';
537
}
538
} else {
539
if (this.state === SashState.AtMinimum) {
540
cursor = 'e-resize';
541
} else if (this.state === SashState.AtMaximum) {
542
cursor = 'w-resize';
543
} else {
544
cursor = isMacintosh ? 'col-resize' : 'ew-resize';
545
}
546
}
547
548
style.textContent = `* { cursor: ${cursor} !important; }`;
549
};
550
551
const disposables = new DisposableStore();
552
553
updateStyle();
554
555
if (!isMultisashResize) {
556
this.onDidEnablementChange.event(updateStyle, null, disposables);
557
}
558
559
const onPointerMove = (e: PointerEvent) => {
560
EventHelper.stop(e, false);
561
const event: ISashEvent = { startX, currentX: e.pageX, startY, currentY: e.pageY, altKey };
562
563
this._onDidChange.fire(event);
564
};
565
566
const onPointerUp = (e: PointerEvent) => {
567
EventHelper.stop(e, false);
568
569
style.remove();
570
571
this.el.classList.remove('active');
572
this._onDidEnd.fire();
573
574
disposables.dispose();
575
576
for (const iframe of iframes) {
577
iframe.classList.remove(PointerEventsDisabledCssClass);
578
}
579
};
580
581
pointerEventFactory.onPointerMove(onPointerMove, null, disposables);
582
pointerEventFactory.onPointerUp(onPointerUp, null, disposables);
583
disposables.add(pointerEventFactory);
584
}
585
586
private onPointerDoublePress(e: MouseEvent): void {
587
const orthogonalSash = this.getOrthogonalSash(e);
588
589
if (orthogonalSash) {
590
orthogonalSash._onDidReset.fire();
591
}
592
593
if (this.linkedSash) {
594
this.linkedSash._onDidReset.fire();
595
}
596
597
this._onDidReset.fire();
598
}
599
600
private static onMouseEnter(sash: Sash, fromLinkedSash: boolean = false): void {
601
if (sash.el.classList.contains('active')) {
602
sash.hoverDelayer.cancel();
603
sash.el.classList.add('hover');
604
} else {
605
sash.hoverDelayer.trigger(() => sash.el.classList.add('hover'), sash.hoverDelay).then(undefined, () => { });
606
}
607
608
if (!fromLinkedSash && sash.linkedSash) {
609
Sash.onMouseEnter(sash.linkedSash, true);
610
}
611
}
612
613
private static onMouseLeave(sash: Sash, fromLinkedSash: boolean = false): void {
614
sash.hoverDelayer.cancel();
615
sash.el.classList.remove('hover');
616
617
if (!fromLinkedSash && sash.linkedSash) {
618
Sash.onMouseLeave(sash.linkedSash, true);
619
}
620
}
621
622
/**
623
* Forcefully stop any user interactions with this sash.
624
* Useful when hiding a parent component, while the user is still
625
* interacting with the sash.
626
*/
627
clearSashHoverState(): void {
628
Sash.onMouseLeave(this);
629
}
630
631
/**
632
* Layout the sash. The sash will size and position itself
633
* based on its provided {@link ISashLayoutProvider layout provider}.
634
*/
635
layout(): void {
636
if (this.orientation === Orientation.VERTICAL) {
637
const verticalProvider = (<IVerticalSashLayoutProvider>this.layoutProvider);
638
this.el.style.left = verticalProvider.getVerticalSashLeft(this) - (this.size / 2) + 'px';
639
640
if (verticalProvider.getVerticalSashTop) {
641
this.el.style.top = verticalProvider.getVerticalSashTop(this) + 'px';
642
}
643
644
if (verticalProvider.getVerticalSashHeight) {
645
this.el.style.height = verticalProvider.getVerticalSashHeight(this) + 'px';
646
}
647
} else {
648
const horizontalProvider = (<IHorizontalSashLayoutProvider>this.layoutProvider);
649
this.el.style.top = horizontalProvider.getHorizontalSashTop(this) - (this.size / 2) + 'px';
650
651
if (horizontalProvider.getHorizontalSashLeft) {
652
this.el.style.left = horizontalProvider.getHorizontalSashLeft(this) + 'px';
653
}
654
655
if (horizontalProvider.getHorizontalSashWidth) {
656
this.el.style.width = horizontalProvider.getHorizontalSashWidth(this) + 'px';
657
}
658
}
659
}
660
661
private getOrthogonalSash(e: PointerEvent): Sash | undefined {
662
const target = e.initialTarget ?? e.target;
663
664
if (!target || !(isHTMLElement(target))) {
665
return undefined;
666
}
667
668
if (target.classList.contains('orthogonal-drag-handle')) {
669
return target.classList.contains('start') ? this.orthogonalStartSash : this.orthogonalEndSash;
670
}
671
672
return undefined;
673
}
674
675
override dispose(): void {
676
super.dispose();
677
this.el.remove();
678
}
679
}
680
681