539ee24b771482bafdc6b54197d8c46c0272ed72eaf0ac08229942fcc845b40e79f02ab644f5e5db21b71aa9b2ac4ee061b5f66f552ab40992d370380cc480 34 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751
  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. import { $, addDisposableListener, append, scheduleAtNextAnimationFrame } from '../../dom.js';
  6. import { Sash } from '../sash/sash.js';
  7. import { SmoothScrollableElement } from '../scrollbar/scrollableElement.js';
  8. import { pushToEnd, pushToStart, range } from '../../../common/arrays.js';
  9. import { Color } from '../../../common/color.js';
  10. import { Emitter, Event } from '../../../common/event.js';
  11. import { combinedDisposable, Disposable, dispose, toDisposable } from '../../../common/lifecycle.js';
  12. import { clamp } from '../../../common/numbers.js';
  13. import { Scrollable } from '../../../common/scrollable.js';
  14. import * as types from '../../../common/types.js';
  15. import './splitview.css';
  16. const defaultStyles = {
  17. separatorBorder: Color.transparent
  18. };
  19. class ViewItem {
  20. constructor(container, view, size, disposable) {
  21. this.container = container;
  22. this.view = view;
  23. this.disposable = disposable;
  24. this._cachedVisibleSize = undefined;
  25. if (typeof size === 'number') {
  26. this._size = size;
  27. this._cachedVisibleSize = undefined;
  28. container.classList.add('visible');
  29. }
  30. else {
  31. this._size = 0;
  32. this._cachedVisibleSize = size.cachedVisibleSize;
  33. }
  34. }
  35. set size(size) {
  36. this._size = size;
  37. }
  38. get size() {
  39. return this._size;
  40. }
  41. get visible() {
  42. return typeof this._cachedVisibleSize === 'undefined';
  43. }
  44. setVisible(visible, size) {
  45. var _a, _b;
  46. if (visible === this.visible) {
  47. return;
  48. }
  49. if (visible) {
  50. this.size = clamp(this._cachedVisibleSize, this.viewMinimumSize, this.viewMaximumSize);
  51. this._cachedVisibleSize = undefined;
  52. }
  53. else {
  54. this._cachedVisibleSize = typeof size === 'number' ? size : this.size;
  55. this.size = 0;
  56. }
  57. this.container.classList.toggle('visible', visible);
  58. (_b = (_a = this.view).setVisible) === null || _b === void 0 ? void 0 : _b.call(_a, visible);
  59. }
  60. get minimumSize() { return this.visible ? this.view.minimumSize : 0; }
  61. get viewMinimumSize() { return this.view.minimumSize; }
  62. get maximumSize() { return this.visible ? this.view.maximumSize : 0; }
  63. get viewMaximumSize() { return this.view.maximumSize; }
  64. get priority() { return this.view.priority; }
  65. get snap() { return !!this.view.snap; }
  66. set enabled(enabled) {
  67. this.container.style.pointerEvents = enabled ? '' : 'none';
  68. }
  69. layout(offset, layoutContext) {
  70. this.layoutContainer(offset);
  71. this.view.layout(this.size, offset, layoutContext);
  72. }
  73. dispose() {
  74. this.disposable.dispose();
  75. return this.view;
  76. }
  77. }
  78. class VerticalViewItem extends ViewItem {
  79. layoutContainer(offset) {
  80. this.container.style.top = `${offset}px`;
  81. this.container.style.height = `${this.size}px`;
  82. }
  83. }
  84. class HorizontalViewItem extends ViewItem {
  85. layoutContainer(offset) {
  86. this.container.style.left = `${offset}px`;
  87. this.container.style.width = `${this.size}px`;
  88. }
  89. }
  90. var State;
  91. (function (State) {
  92. State[State["Idle"] = 0] = "Idle";
  93. State[State["Busy"] = 1] = "Busy";
  94. })(State || (State = {}));
  95. export var Sizing;
  96. (function (Sizing) {
  97. /**
  98. * When adding or removing views, distribute the delta space among
  99. * all other views.
  100. */
  101. Sizing.Distribute = { type: 'distribute' };
  102. /**
  103. * When adding or removing views, split the delta space with another
  104. * specific view, indexed by the provided `index`.
  105. */
  106. function Split(index) { return { type: 'split', index }; }
  107. Sizing.Split = Split;
  108. /**
  109. * When adding or removing views, assume the view is invisible.
  110. */
  111. function Invisible(cachedVisibleSize) { return { type: 'invisible', cachedVisibleSize }; }
  112. Sizing.Invisible = Invisible;
  113. })(Sizing || (Sizing = {}));
  114. /**
  115. * The {@link SplitView} is the UI component which implements a one dimensional
  116. * flex-like layout algorithm for a collection of {@link IView} instances, which
  117. * are essentially HTMLElement instances with the following size constraints:
  118. *
  119. * - {@link IView.minimumSize}
  120. * - {@link IView.maximumSize}
  121. * - {@link IView.priority}
  122. * - {@link IView.snap}
  123. *
  124. * In case the SplitView doesn't have enough size to fit all views, it will overflow
  125. * its content with a scrollbar.
  126. *
  127. * In between each pair of views there will be a {@link Sash} allowing the user
  128. * to resize the views, making sure the constraints are respected.
  129. *
  130. * An optional {@link TLayoutContext layout context type} may be used in order to
  131. * pass along layout contextual data from the {@link SplitView.layout} method down
  132. * to each view's {@link IView.layout} calls.
  133. *
  134. * Features:
  135. * - Flex-like layout algorithm
  136. * - Snap support
  137. * - Orthogonal sash support, for corner sashes
  138. * - View hide/show support
  139. * - View swap/move support
  140. * - Alt key modifier behavior, macOS style
  141. */
  142. export class SplitView extends Disposable {
  143. /**
  144. * Create a new {@link SplitView} instance.
  145. */
  146. constructor(container, options = {}) {
  147. var _a, _b, _c, _d, _e;
  148. super();
  149. this.size = 0;
  150. this.contentSize = 0;
  151. this.proportions = undefined;
  152. this.viewItems = [];
  153. this.sashItems = [];
  154. this.state = State.Idle;
  155. this._onDidSashChange = this._register(new Emitter());
  156. this._onDidSashReset = this._register(new Emitter());
  157. this._startSnappingEnabled = true;
  158. this._endSnappingEnabled = true;
  159. /**
  160. * Fires whenever the user resizes a {@link Sash sash}.
  161. */
  162. this.onDidSashChange = this._onDidSashChange.event;
  163. /**
  164. * Fires whenever the user double clicks a {@link Sash sash}.
  165. */
  166. this.onDidSashReset = this._onDidSashReset.event;
  167. this.orientation = (_a = options.orientation) !== null && _a !== void 0 ? _a : 0 /* Orientation.VERTICAL */;
  168. this.inverseAltBehavior = (_b = options.inverseAltBehavior) !== null && _b !== void 0 ? _b : false;
  169. this.proportionalLayout = (_c = options.proportionalLayout) !== null && _c !== void 0 ? _c : true;
  170. this.getSashOrthogonalSize = options.getSashOrthogonalSize;
  171. this.el = document.createElement('div');
  172. this.el.classList.add('monaco-split-view2');
  173. this.el.classList.add(this.orientation === 0 /* Orientation.VERTICAL */ ? 'vertical' : 'horizontal');
  174. container.appendChild(this.el);
  175. this.sashContainer = append(this.el, $('.sash-container'));
  176. this.viewContainer = $('.split-view-container');
  177. this.scrollable = new Scrollable({
  178. forceIntegerValues: true,
  179. smoothScrollDuration: 125,
  180. scheduleAtNextAnimationFrame
  181. });
  182. this.scrollableElement = this._register(new SmoothScrollableElement(this.viewContainer, {
  183. vertical: this.orientation === 0 /* Orientation.VERTICAL */ ? ((_d = options.scrollbarVisibility) !== null && _d !== void 0 ? _d : 1 /* ScrollbarVisibility.Auto */) : 2 /* ScrollbarVisibility.Hidden */,
  184. horizontal: this.orientation === 1 /* Orientation.HORIZONTAL */ ? ((_e = options.scrollbarVisibility) !== null && _e !== void 0 ? _e : 1 /* ScrollbarVisibility.Auto */) : 2 /* ScrollbarVisibility.Hidden */
  185. }, this.scrollable));
  186. this.onDidScroll = this.scrollableElement.onScroll;
  187. this._register(this.onDidScroll(e => {
  188. this.viewContainer.scrollTop = e.scrollTop;
  189. this.viewContainer.scrollLeft = e.scrollLeft;
  190. }));
  191. append(this.el, this.scrollableElement.getDomNode());
  192. this.style(options.styles || defaultStyles);
  193. // We have an existing set of view, add them now
  194. if (options.descriptor) {
  195. this.size = options.descriptor.size;
  196. options.descriptor.views.forEach((viewDescriptor, index) => {
  197. const sizing = types.isUndefined(viewDescriptor.visible) || viewDescriptor.visible ? viewDescriptor.size : { type: 'invisible', cachedVisibleSize: viewDescriptor.size };
  198. const view = viewDescriptor.view;
  199. this.doAddView(view, sizing, index, true);
  200. });
  201. // Initialize content size and proportions for first layout
  202. this.contentSize = this.viewItems.reduce((r, i) => r + i.size, 0);
  203. this.saveProportions();
  204. }
  205. }
  206. get orthogonalStartSash() { return this._orthogonalStartSash; }
  207. get orthogonalEndSash() { return this._orthogonalEndSash; }
  208. get startSnappingEnabled() { return this._startSnappingEnabled; }
  209. get endSnappingEnabled() { return this._endSnappingEnabled; }
  210. /**
  211. * A reference to a sash, perpendicular to all sashes in this {@link SplitView},
  212. * located at the left- or top-most side of the SplitView.
  213. * Corner sashes will be created automatically at the intersections.
  214. */
  215. set orthogonalStartSash(sash) {
  216. for (const sashItem of this.sashItems) {
  217. sashItem.sash.orthogonalStartSash = sash;
  218. }
  219. this._orthogonalStartSash = sash;
  220. }
  221. /**
  222. * A reference to a sash, perpendicular to all sashes in this {@link SplitView},
  223. * located at the right- or bottom-most side of the SplitView.
  224. * Corner sashes will be created automatically at the intersections.
  225. */
  226. set orthogonalEndSash(sash) {
  227. for (const sashItem of this.sashItems) {
  228. sashItem.sash.orthogonalEndSash = sash;
  229. }
  230. this._orthogonalEndSash = sash;
  231. }
  232. /**
  233. * Enable/disable snapping at the beginning of this {@link SplitView}.
  234. */
  235. set startSnappingEnabled(startSnappingEnabled) {
  236. if (this._startSnappingEnabled === startSnappingEnabled) {
  237. return;
  238. }
  239. this._startSnappingEnabled = startSnappingEnabled;
  240. this.updateSashEnablement();
  241. }
  242. /**
  243. * Enable/disable snapping at the end of this {@link SplitView}.
  244. */
  245. set endSnappingEnabled(endSnappingEnabled) {
  246. if (this._endSnappingEnabled === endSnappingEnabled) {
  247. return;
  248. }
  249. this._endSnappingEnabled = endSnappingEnabled;
  250. this.updateSashEnablement();
  251. }
  252. style(styles) {
  253. if (styles.separatorBorder.isTransparent()) {
  254. this.el.classList.remove('separator-border');
  255. this.el.style.removeProperty('--separator-border');
  256. }
  257. else {
  258. this.el.classList.add('separator-border');
  259. this.el.style.setProperty('--separator-border', styles.separatorBorder.toString());
  260. }
  261. }
  262. /**
  263. * Add a {@link IView view} to this {@link SplitView}.
  264. *
  265. * @param view The view to add.
  266. * @param size Either a fixed size, or a dynamic {@link Sizing} strategy.
  267. * @param index The index to insert the view on.
  268. * @param skipLayout Whether layout should be skipped.
  269. */
  270. addView(view, size, index = this.viewItems.length, skipLayout) {
  271. this.doAddView(view, size, index, skipLayout);
  272. }
  273. /**
  274. * Layout the {@link SplitView}.
  275. *
  276. * @param size The entire size of the {@link SplitView}.
  277. * @param layoutContext An optional layout context to pass along to {@link IView views}.
  278. */
  279. layout(size, layoutContext) {
  280. const previousSize = Math.max(this.size, this.contentSize);
  281. this.size = size;
  282. this.layoutContext = layoutContext;
  283. if (!this.proportions) {
  284. const indexes = range(this.viewItems.length);
  285. const lowPriorityIndexes = indexes.filter(i => this.viewItems[i].priority === 1 /* LayoutPriority.Low */);
  286. const highPriorityIndexes = indexes.filter(i => this.viewItems[i].priority === 2 /* LayoutPriority.High */);
  287. this.resize(this.viewItems.length - 1, size - previousSize, undefined, lowPriorityIndexes, highPriorityIndexes);
  288. }
  289. else {
  290. for (let i = 0; i < this.viewItems.length; i++) {
  291. const item = this.viewItems[i];
  292. item.size = clamp(Math.round(this.proportions[i] * size), item.minimumSize, item.maximumSize);
  293. }
  294. }
  295. this.distributeEmptySpace();
  296. this.layoutViews();
  297. }
  298. saveProportions() {
  299. if (this.proportionalLayout && this.contentSize > 0) {
  300. this.proportions = this.viewItems.map(i => i.size / this.contentSize);
  301. }
  302. }
  303. onSashStart({ sash, start, alt }) {
  304. for (const item of this.viewItems) {
  305. item.enabled = false;
  306. }
  307. const index = this.sashItems.findIndex(item => item.sash === sash);
  308. // This way, we can press Alt while we resize a sash, macOS style!
  309. const disposable = combinedDisposable(addDisposableListener(document.body, 'keydown', e => resetSashDragState(this.sashDragState.current, e.altKey)), addDisposableListener(document.body, 'keyup', () => resetSashDragState(this.sashDragState.current, false)));
  310. const resetSashDragState = (start, alt) => {
  311. const sizes = this.viewItems.map(i => i.size);
  312. let minDelta = Number.NEGATIVE_INFINITY;
  313. let maxDelta = Number.POSITIVE_INFINITY;
  314. if (this.inverseAltBehavior) {
  315. alt = !alt;
  316. }
  317. if (alt) {
  318. // When we're using the last sash with Alt, we're resizing
  319. // the view to the left/up, instead of right/down as usual
  320. // Thus, we must do the inverse of the usual
  321. const isLastSash = index === this.sashItems.length - 1;
  322. if (isLastSash) {
  323. const viewItem = this.viewItems[index];
  324. minDelta = (viewItem.minimumSize - viewItem.size) / 2;
  325. maxDelta = (viewItem.maximumSize - viewItem.size) / 2;
  326. }
  327. else {
  328. const viewItem = this.viewItems[index + 1];
  329. minDelta = (viewItem.size - viewItem.maximumSize) / 2;
  330. maxDelta = (viewItem.size - viewItem.minimumSize) / 2;
  331. }
  332. }
  333. let snapBefore;
  334. let snapAfter;
  335. if (!alt) {
  336. const upIndexes = range(index, -1);
  337. const downIndexes = range(index + 1, this.viewItems.length);
  338. const minDeltaUp = upIndexes.reduce((r, i) => r + (this.viewItems[i].minimumSize - sizes[i]), 0);
  339. const maxDeltaUp = upIndexes.reduce((r, i) => r + (this.viewItems[i].viewMaximumSize - sizes[i]), 0);
  340. const maxDeltaDown = downIndexes.length === 0 ? Number.POSITIVE_INFINITY : downIndexes.reduce((r, i) => r + (sizes[i] - this.viewItems[i].minimumSize), 0);
  341. const minDeltaDown = downIndexes.length === 0 ? Number.NEGATIVE_INFINITY : downIndexes.reduce((r, i) => r + (sizes[i] - this.viewItems[i].viewMaximumSize), 0);
  342. const minDelta = Math.max(minDeltaUp, minDeltaDown);
  343. const maxDelta = Math.min(maxDeltaDown, maxDeltaUp);
  344. const snapBeforeIndex = this.findFirstSnapIndex(upIndexes);
  345. const snapAfterIndex = this.findFirstSnapIndex(downIndexes);
  346. if (typeof snapBeforeIndex === 'number') {
  347. const viewItem = this.viewItems[snapBeforeIndex];
  348. const halfSize = Math.floor(viewItem.viewMinimumSize / 2);
  349. snapBefore = {
  350. index: snapBeforeIndex,
  351. limitDelta: viewItem.visible ? minDelta - halfSize : minDelta + halfSize,
  352. size: viewItem.size
  353. };
  354. }
  355. if (typeof snapAfterIndex === 'number') {
  356. const viewItem = this.viewItems[snapAfterIndex];
  357. const halfSize = Math.floor(viewItem.viewMinimumSize / 2);
  358. snapAfter = {
  359. index: snapAfterIndex,
  360. limitDelta: viewItem.visible ? maxDelta + halfSize : maxDelta - halfSize,
  361. size: viewItem.size
  362. };
  363. }
  364. }
  365. this.sashDragState = { start, current: start, index, sizes, minDelta, maxDelta, alt, snapBefore, snapAfter, disposable };
  366. };
  367. resetSashDragState(start, alt);
  368. }
  369. onSashChange({ current }) {
  370. const { index, start, sizes, alt, minDelta, maxDelta, snapBefore, snapAfter } = this.sashDragState;
  371. this.sashDragState.current = current;
  372. const delta = current - start;
  373. const newDelta = this.resize(index, delta, sizes, undefined, undefined, minDelta, maxDelta, snapBefore, snapAfter);
  374. if (alt) {
  375. const isLastSash = index === this.sashItems.length - 1;
  376. const newSizes = this.viewItems.map(i => i.size);
  377. const viewItemIndex = isLastSash ? index : index + 1;
  378. const viewItem = this.viewItems[viewItemIndex];
  379. const newMinDelta = viewItem.size - viewItem.maximumSize;
  380. const newMaxDelta = viewItem.size - viewItem.minimumSize;
  381. const resizeIndex = isLastSash ? index - 1 : index + 1;
  382. this.resize(resizeIndex, -newDelta, newSizes, undefined, undefined, newMinDelta, newMaxDelta);
  383. }
  384. this.distributeEmptySpace();
  385. this.layoutViews();
  386. }
  387. onSashEnd(index) {
  388. this._onDidSashChange.fire(index);
  389. this.sashDragState.disposable.dispose();
  390. this.saveProportions();
  391. for (const item of this.viewItems) {
  392. item.enabled = true;
  393. }
  394. }
  395. onViewChange(item, size) {
  396. const index = this.viewItems.indexOf(item);
  397. if (index < 0 || index >= this.viewItems.length) {
  398. return;
  399. }
  400. size = typeof size === 'number' ? size : item.size;
  401. size = clamp(size, item.minimumSize, item.maximumSize);
  402. if (this.inverseAltBehavior && index > 0) {
  403. // In this case, we want the view to grow or shrink both sides equally
  404. // so we just resize the "left" side by half and let `resize` do the clamping magic
  405. this.resize(index - 1, Math.floor((item.size - size) / 2));
  406. this.distributeEmptySpace();
  407. this.layoutViews();
  408. }
  409. else {
  410. item.size = size;
  411. this.relayout([index], undefined);
  412. }
  413. }
  414. /**
  415. * Resize a {@link IView view} within the {@link SplitView}.
  416. *
  417. * @param index The {@link IView view} index.
  418. * @param size The {@link IView view} size.
  419. */
  420. resizeView(index, size) {
  421. if (this.state !== State.Idle) {
  422. throw new Error('Cant modify splitview');
  423. }
  424. this.state = State.Busy;
  425. if (index < 0 || index >= this.viewItems.length) {
  426. return;
  427. }
  428. const indexes = range(this.viewItems.length).filter(i => i !== index);
  429. const lowPriorityIndexes = [...indexes.filter(i => this.viewItems[i].priority === 1 /* LayoutPriority.Low */), index];
  430. const highPriorityIndexes = indexes.filter(i => this.viewItems[i].priority === 2 /* LayoutPriority.High */);
  431. const item = this.viewItems[index];
  432. size = Math.round(size);
  433. size = clamp(size, item.minimumSize, Math.min(item.maximumSize, this.size));
  434. item.size = size;
  435. this.relayout(lowPriorityIndexes, highPriorityIndexes);
  436. this.state = State.Idle;
  437. }
  438. /**
  439. * Distribute the entire {@link SplitView} size among all {@link IView views}.
  440. */
  441. distributeViewSizes() {
  442. const flexibleViewItems = [];
  443. let flexibleSize = 0;
  444. for (const item of this.viewItems) {
  445. if (item.maximumSize - item.minimumSize > 0) {
  446. flexibleViewItems.push(item);
  447. flexibleSize += item.size;
  448. }
  449. }
  450. const size = Math.floor(flexibleSize / flexibleViewItems.length);
  451. for (const item of flexibleViewItems) {
  452. item.size = clamp(size, item.minimumSize, item.maximumSize);
  453. }
  454. const indexes = range(this.viewItems.length);
  455. const lowPriorityIndexes = indexes.filter(i => this.viewItems[i].priority === 1 /* LayoutPriority.Low */);
  456. const highPriorityIndexes = indexes.filter(i => this.viewItems[i].priority === 2 /* LayoutPriority.High */);
  457. this.relayout(lowPriorityIndexes, highPriorityIndexes);
  458. }
  459. /**
  460. * Returns the size of a {@link IView view}.
  461. */
  462. getViewSize(index) {
  463. if (index < 0 || index >= this.viewItems.length) {
  464. return -1;
  465. }
  466. return this.viewItems[index].size;
  467. }
  468. doAddView(view, size, index = this.viewItems.length, skipLayout) {
  469. if (this.state !== State.Idle) {
  470. throw new Error('Cant modify splitview');
  471. }
  472. this.state = State.Busy;
  473. // Add view
  474. const container = $('.split-view-view');
  475. if (index === this.viewItems.length) {
  476. this.viewContainer.appendChild(container);
  477. }
  478. else {
  479. this.viewContainer.insertBefore(container, this.viewContainer.children.item(index));
  480. }
  481. const onChangeDisposable = view.onDidChange(size => this.onViewChange(item, size));
  482. const containerDisposable = toDisposable(() => this.viewContainer.removeChild(container));
  483. const disposable = combinedDisposable(onChangeDisposable, containerDisposable);
  484. let viewSize;
  485. if (typeof size === 'number') {
  486. viewSize = size;
  487. }
  488. else if (size.type === 'split') {
  489. viewSize = this.getViewSize(size.index) / 2;
  490. }
  491. else if (size.type === 'invisible') {
  492. viewSize = { cachedVisibleSize: size.cachedVisibleSize };
  493. }
  494. else {
  495. viewSize = view.minimumSize;
  496. }
  497. const item = this.orientation === 0 /* Orientation.VERTICAL */
  498. ? new VerticalViewItem(container, view, viewSize, disposable)
  499. : new HorizontalViewItem(container, view, viewSize, disposable);
  500. this.viewItems.splice(index, 0, item);
  501. // Add sash
  502. if (this.viewItems.length > 1) {
  503. const opts = { orthogonalStartSash: this.orthogonalStartSash, orthogonalEndSash: this.orthogonalEndSash };
  504. const sash = this.orientation === 0 /* Orientation.VERTICAL */
  505. ? new Sash(this.sashContainer, { getHorizontalSashTop: s => this.getSashPosition(s), getHorizontalSashWidth: this.getSashOrthogonalSize }, Object.assign(Object.assign({}, opts), { orientation: 1 /* Orientation.HORIZONTAL */ }))
  506. : new Sash(this.sashContainer, { getVerticalSashLeft: s => this.getSashPosition(s), getVerticalSashHeight: this.getSashOrthogonalSize }, Object.assign(Object.assign({}, opts), { orientation: 0 /* Orientation.VERTICAL */ }));
  507. const sashEventMapper = this.orientation === 0 /* Orientation.VERTICAL */
  508. ? (e) => ({ sash, start: e.startY, current: e.currentY, alt: e.altKey })
  509. : (e) => ({ sash, start: e.startX, current: e.currentX, alt: e.altKey });
  510. const onStart = Event.map(sash.onDidStart, sashEventMapper);
  511. const onStartDisposable = onStart(this.onSashStart, this);
  512. const onChange = Event.map(sash.onDidChange, sashEventMapper);
  513. const onChangeDisposable = onChange(this.onSashChange, this);
  514. const onEnd = Event.map(sash.onDidEnd, () => this.sashItems.findIndex(item => item.sash === sash));
  515. const onEndDisposable = onEnd(this.onSashEnd, this);
  516. const onDidResetDisposable = sash.onDidReset(() => {
  517. const index = this.sashItems.findIndex(item => item.sash === sash);
  518. const upIndexes = range(index, -1);
  519. const downIndexes = range(index + 1, this.viewItems.length);
  520. const snapBeforeIndex = this.findFirstSnapIndex(upIndexes);
  521. const snapAfterIndex = this.findFirstSnapIndex(downIndexes);
  522. if (typeof snapBeforeIndex === 'number' && !this.viewItems[snapBeforeIndex].visible) {
  523. return;
  524. }
  525. if (typeof snapAfterIndex === 'number' && !this.viewItems[snapAfterIndex].visible) {
  526. return;
  527. }
  528. this._onDidSashReset.fire(index);
  529. });
  530. const disposable = combinedDisposable(onStartDisposable, onChangeDisposable, onEndDisposable, onDidResetDisposable, sash);
  531. const sashItem = { sash, disposable };
  532. this.sashItems.splice(index - 1, 0, sashItem);
  533. }
  534. container.appendChild(view.element);
  535. let highPriorityIndexes;
  536. if (typeof size !== 'number' && size.type === 'split') {
  537. highPriorityIndexes = [size.index];
  538. }
  539. if (!skipLayout) {
  540. this.relayout([index], highPriorityIndexes);
  541. }
  542. this.state = State.Idle;
  543. if (!skipLayout && typeof size !== 'number' && size.type === 'distribute') {
  544. this.distributeViewSizes();
  545. }
  546. }
  547. relayout(lowPriorityIndexes, highPriorityIndexes) {
  548. const contentSize = this.viewItems.reduce((r, i) => r + i.size, 0);
  549. this.resize(this.viewItems.length - 1, this.size - contentSize, undefined, lowPriorityIndexes, highPriorityIndexes);
  550. this.distributeEmptySpace();
  551. this.layoutViews();
  552. this.saveProportions();
  553. }
  554. resize(index, delta, sizes = this.viewItems.map(i => i.size), lowPriorityIndexes, highPriorityIndexes, overloadMinDelta = Number.NEGATIVE_INFINITY, overloadMaxDelta = Number.POSITIVE_INFINITY, snapBefore, snapAfter) {
  555. if (index < 0 || index >= this.viewItems.length) {
  556. return 0;
  557. }
  558. const upIndexes = range(index, -1);
  559. const downIndexes = range(index + 1, this.viewItems.length);
  560. if (highPriorityIndexes) {
  561. for (const index of highPriorityIndexes) {
  562. pushToStart(upIndexes, index);
  563. pushToStart(downIndexes, index);
  564. }
  565. }
  566. if (lowPriorityIndexes) {
  567. for (const index of lowPriorityIndexes) {
  568. pushToEnd(upIndexes, index);
  569. pushToEnd(downIndexes, index);
  570. }
  571. }
  572. const upItems = upIndexes.map(i => this.viewItems[i]);
  573. const upSizes = upIndexes.map(i => sizes[i]);
  574. const downItems = downIndexes.map(i => this.viewItems[i]);
  575. const downSizes = downIndexes.map(i => sizes[i]);
  576. const minDeltaUp = upIndexes.reduce((r, i) => r + (this.viewItems[i].minimumSize - sizes[i]), 0);
  577. const maxDeltaUp = upIndexes.reduce((r, i) => r + (this.viewItems[i].maximumSize - sizes[i]), 0);
  578. const maxDeltaDown = downIndexes.length === 0 ? Number.POSITIVE_INFINITY : downIndexes.reduce((r, i) => r + (sizes[i] - this.viewItems[i].minimumSize), 0);
  579. const minDeltaDown = downIndexes.length === 0 ? Number.NEGATIVE_INFINITY : downIndexes.reduce((r, i) => r + (sizes[i] - this.viewItems[i].maximumSize), 0);
  580. const minDelta = Math.max(minDeltaUp, minDeltaDown, overloadMinDelta);
  581. const maxDelta = Math.min(maxDeltaDown, maxDeltaUp, overloadMaxDelta);
  582. let snapped = false;
  583. if (snapBefore) {
  584. const snapView = this.viewItems[snapBefore.index];
  585. const visible = delta >= snapBefore.limitDelta;
  586. snapped = visible !== snapView.visible;
  587. snapView.setVisible(visible, snapBefore.size);
  588. }
  589. if (!snapped && snapAfter) {
  590. const snapView = this.viewItems[snapAfter.index];
  591. const visible = delta < snapAfter.limitDelta;
  592. snapped = visible !== snapView.visible;
  593. snapView.setVisible(visible, snapAfter.size);
  594. }
  595. if (snapped) {
  596. return this.resize(index, delta, sizes, lowPriorityIndexes, highPriorityIndexes, overloadMinDelta, overloadMaxDelta);
  597. }
  598. delta = clamp(delta, minDelta, maxDelta);
  599. for (let i = 0, deltaUp = delta; i < upItems.length; i++) {
  600. const item = upItems[i];
  601. const size = clamp(upSizes[i] + deltaUp, item.minimumSize, item.maximumSize);
  602. const viewDelta = size - upSizes[i];
  603. deltaUp -= viewDelta;
  604. item.size = size;
  605. }
  606. for (let i = 0, deltaDown = delta; i < downItems.length; i++) {
  607. const item = downItems[i];
  608. const size = clamp(downSizes[i] - deltaDown, item.minimumSize, item.maximumSize);
  609. const viewDelta = size - downSizes[i];
  610. deltaDown += viewDelta;
  611. item.size = size;
  612. }
  613. return delta;
  614. }
  615. distributeEmptySpace(lowPriorityIndex) {
  616. const contentSize = this.viewItems.reduce((r, i) => r + i.size, 0);
  617. let emptyDelta = this.size - contentSize;
  618. const indexes = range(this.viewItems.length - 1, -1);
  619. const lowPriorityIndexes = indexes.filter(i => this.viewItems[i].priority === 1 /* LayoutPriority.Low */);
  620. const highPriorityIndexes = indexes.filter(i => this.viewItems[i].priority === 2 /* LayoutPriority.High */);
  621. for (const index of highPriorityIndexes) {
  622. pushToStart(indexes, index);
  623. }
  624. for (const index of lowPriorityIndexes) {
  625. pushToEnd(indexes, index);
  626. }
  627. if (typeof lowPriorityIndex === 'number') {
  628. pushToEnd(indexes, lowPriorityIndex);
  629. }
  630. for (let i = 0; emptyDelta !== 0 && i < indexes.length; i++) {
  631. const item = this.viewItems[indexes[i]];
  632. const size = clamp(item.size + emptyDelta, item.minimumSize, item.maximumSize);
  633. const viewDelta = size - item.size;
  634. emptyDelta -= viewDelta;
  635. item.size = size;
  636. }
  637. }
  638. layoutViews() {
  639. // Save new content size
  640. this.contentSize = this.viewItems.reduce((r, i) => r + i.size, 0);
  641. // Layout views
  642. let offset = 0;
  643. for (const viewItem of this.viewItems) {
  644. viewItem.layout(offset, this.layoutContext);
  645. offset += viewItem.size;
  646. }
  647. // Layout sashes
  648. this.sashItems.forEach(item => item.sash.layout());
  649. this.updateSashEnablement();
  650. this.updateScrollableElement();
  651. }
  652. updateScrollableElement() {
  653. if (this.orientation === 0 /* Orientation.VERTICAL */) {
  654. this.scrollableElement.setScrollDimensions({
  655. height: this.size,
  656. scrollHeight: this.contentSize
  657. });
  658. }
  659. else {
  660. this.scrollableElement.setScrollDimensions({
  661. width: this.size,
  662. scrollWidth: this.contentSize
  663. });
  664. }
  665. }
  666. updateSashEnablement() {
  667. let previous = false;
  668. const collapsesDown = this.viewItems.map(i => previous = (i.size - i.minimumSize > 0) || previous);
  669. previous = false;
  670. const expandsDown = this.viewItems.map(i => previous = (i.maximumSize - i.size > 0) || previous);
  671. const reverseViews = [...this.viewItems].reverse();
  672. previous = false;
  673. const collapsesUp = reverseViews.map(i => previous = (i.size - i.minimumSize > 0) || previous).reverse();
  674. previous = false;
  675. const expandsUp = reverseViews.map(i => previous = (i.maximumSize - i.size > 0) || previous).reverse();
  676. let position = 0;
  677. for (let index = 0; index < this.sashItems.length; index++) {
  678. const { sash } = this.sashItems[index];
  679. const viewItem = this.viewItems[index];
  680. position += viewItem.size;
  681. const min = !(collapsesDown[index] && expandsUp[index + 1]);
  682. const max = !(expandsDown[index] && collapsesUp[index + 1]);
  683. if (min && max) {
  684. const upIndexes = range(index, -1);
  685. const downIndexes = range(index + 1, this.viewItems.length);
  686. const snapBeforeIndex = this.findFirstSnapIndex(upIndexes);
  687. const snapAfterIndex = this.findFirstSnapIndex(downIndexes);
  688. const snappedBefore = typeof snapBeforeIndex === 'number' && !this.viewItems[snapBeforeIndex].visible;
  689. const snappedAfter = typeof snapAfterIndex === 'number' && !this.viewItems[snapAfterIndex].visible;
  690. if (snappedBefore && collapsesUp[index] && (position > 0 || this.startSnappingEnabled)) {
  691. sash.state = 1 /* SashState.AtMinimum */;
  692. }
  693. else if (snappedAfter && collapsesDown[index] && (position < this.contentSize || this.endSnappingEnabled)) {
  694. sash.state = 2 /* SashState.AtMaximum */;
  695. }
  696. else {
  697. sash.state = 0 /* SashState.Disabled */;
  698. }
  699. }
  700. else if (min && !max) {
  701. sash.state = 1 /* SashState.AtMinimum */;
  702. }
  703. else if (!min && max) {
  704. sash.state = 2 /* SashState.AtMaximum */;
  705. }
  706. else {
  707. sash.state = 3 /* SashState.Enabled */;
  708. }
  709. }
  710. }
  711. getSashPosition(sash) {
  712. let position = 0;
  713. for (let i = 0; i < this.sashItems.length; i++) {
  714. position += this.viewItems[i].size;
  715. if (this.sashItems[i].sash === sash) {
  716. return position;
  717. }
  718. }
  719. return 0;
  720. }
  721. findFirstSnapIndex(indexes) {
  722. // visible views first
  723. for (const index of indexes) {
  724. const viewItem = this.viewItems[index];
  725. if (!viewItem.visible) {
  726. continue;
  727. }
  728. if (viewItem.snap) {
  729. return index;
  730. }
  731. }
  732. // then, hidden views
  733. for (const index of indexes) {
  734. const viewItem = this.viewItems[index];
  735. if (viewItem.visible && viewItem.maximumSize - viewItem.minimumSize > 0) {
  736. return undefined;
  737. }
  738. if (!viewItem.visible && viewItem.snap) {
  739. return index;
  740. }
  741. }
  742. return undefined;
  743. }
  744. dispose() {
  745. super.dispose();
  746. dispose(this.viewItems);
  747. this.viewItems = [];
  748. this.sashItems.forEach(i => i.disposable.dispose());
  749. this.sashItems = [];
  750. }
  751. }